osam 0.1.0__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.
- osam-0.1.0/.github/workflows/ci.yml +39 -0
- osam-0.1.0/.github/workflows/release.yml +117 -0
- osam-0.1.0/.readme/dogs_mask.jpg +0 -0
- osam-0.1.0/.readme/dogs_output.jpg +0 -0
- osam-0.1.0/.readme/icon.png +0 -0
- osam-0.1.0/LICENSE +21 -0
- osam-0.1.0/Makefile +27 -0
- osam-0.1.0/PKG-INFO +120 -0
- osam-0.1.0/README.md +81 -0
- osam-0.1.0/examples/_images/dogs.jpg +0 -0
- osam-0.1.0/examples/bash_comparemodels/README.md +7 -0
- osam-0.1.0/examples/bash_comparemodels/comparemodels.sh +30 -0
- osam-0.1.0/osam/__init__.py +3 -0
- osam-0.1.0/osam/__main__.py +154 -0
- osam-0.1.0/osam/_humanize.py +28 -0
- osam-0.1.0/osam/_json.py +19 -0
- osam-0.1.0/osam/_models/__init__.py +14 -0
- osam-0.1.0/osam/_models/_base.py +102 -0
- osam-0.1.0/osam/_models/_efficient_sam.py +87 -0
- osam-0.1.0/osam/_models/_sam.py +184 -0
- osam-0.1.0/osam/_server.py +33 -0
- osam-0.1.0/osam/_tabulate.py +19 -0
- osam-0.1.0/osam/apis.py +40 -0
- osam-0.1.0/osam/types.py +86 -0
- osam-0.1.0/pyproject.toml +102 -0
- osam-0.1.0/tests/test___main__.py +15 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ["3.8", "3.12"]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/setup-python@v4
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- uses: actions/checkout@v3
|
|
22
|
+
|
|
23
|
+
- name: Install main
|
|
24
|
+
run: |
|
|
25
|
+
pip install .[test]
|
|
26
|
+
|
|
27
|
+
- name: Lint
|
|
28
|
+
run: |
|
|
29
|
+
make lint
|
|
30
|
+
|
|
31
|
+
- name: Test
|
|
32
|
+
run: |
|
|
33
|
+
make test
|
|
34
|
+
|
|
35
|
+
- name: Install from dist
|
|
36
|
+
run: |
|
|
37
|
+
make build
|
|
38
|
+
pip install dist/osam-*.tar.gz
|
|
39
|
+
pip install dist/osam-*.whl
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build_binary:
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
11
|
+
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
os: [macos-latest, ubuntu-20.04, windows-latest]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 0
|
|
20
|
+
|
|
21
|
+
- name: Setup Python
|
|
22
|
+
uses: actions/setup-python@v4
|
|
23
|
+
with:
|
|
24
|
+
python-version: '3.10'
|
|
25
|
+
architecture: 'x64'
|
|
26
|
+
cache: 'pip'
|
|
27
|
+
cache-dependency-path: |
|
|
28
|
+
pyproject.toml
|
|
29
|
+
|
|
30
|
+
- name: Install main
|
|
31
|
+
run: |
|
|
32
|
+
pip install .
|
|
33
|
+
|
|
34
|
+
- name: Build Executable
|
|
35
|
+
uses: Nuitka/Nuitka-Action@main
|
|
36
|
+
with:
|
|
37
|
+
nuitka-version: main
|
|
38
|
+
script-name: osam
|
|
39
|
+
onefile: true
|
|
40
|
+
|
|
41
|
+
- name: Test Executable on Linux & macOS
|
|
42
|
+
run: |
|
|
43
|
+
./build/osam.bin --version
|
|
44
|
+
if: matrix.os != 'windows-latest'
|
|
45
|
+
|
|
46
|
+
- name: Test Executable on Windows
|
|
47
|
+
run: |
|
|
48
|
+
./build/osam.exe --version
|
|
49
|
+
if: matrix.os == 'windows-latest'
|
|
50
|
+
|
|
51
|
+
- name: Rename Executable
|
|
52
|
+
run: |
|
|
53
|
+
mv build/osam.bin build/osam-${{ runner.os }}.bin
|
|
54
|
+
if: matrix.os != 'windows-latest'
|
|
55
|
+
|
|
56
|
+
- name: Upload Artifacts
|
|
57
|
+
uses: actions/upload-artifact@v4
|
|
58
|
+
with:
|
|
59
|
+
name: build-${{ runner.os }}
|
|
60
|
+
path: |
|
|
61
|
+
build/*.exe
|
|
62
|
+
build/*.bin
|
|
63
|
+
|
|
64
|
+
release:
|
|
65
|
+
needs: build_binary
|
|
66
|
+
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
|
|
69
|
+
steps:
|
|
70
|
+
- name: Build Changelog
|
|
71
|
+
id: github_release
|
|
72
|
+
uses: mikepenz/release-changelog-builder-action@v3
|
|
73
|
+
env:
|
|
74
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
75
|
+
with:
|
|
76
|
+
configurationJson: |
|
|
77
|
+
{
|
|
78
|
+
"template": "#{{CHANGELOG}}\n\n<details>\n<summary>Uncategorized</summary>\n\n#{{UNCATEGORIZED}}\n</details>\n\n---\n\n- For my daily development update, check [my Twitter/X](https://twitter.com/wkentaro_).\n- If you'd like to support this project, check [my sponsoring page](https://github.com/sponsors/wkentaro).",
|
|
79
|
+
"pr_template": "- #{{TITLE}} ##{{NUMBER}}",
|
|
80
|
+
"categories": [
|
|
81
|
+
{
|
|
82
|
+
"title": "## 🚀 Features",
|
|
83
|
+
"labels": ["feature"]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"title": "## ✨ Enhancement",
|
|
87
|
+
"labels": ["enhancement"]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"title": "## 🐛 Fixes",
|
|
91
|
+
"labels": ["fix"]
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"title": "## 💬 Other",
|
|
95
|
+
"labels": ["other"]
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
- name: Create Changelog
|
|
101
|
+
run: |
|
|
102
|
+
echo '${{steps.github_release.outputs.changelog}}' > ${{ github.workspace }}-CHANGELOG.txt
|
|
103
|
+
|
|
104
|
+
- name: Download Artifact
|
|
105
|
+
uses: actions/download-artifact@v4
|
|
106
|
+
with:
|
|
107
|
+
path: build
|
|
108
|
+
pattern: build-*
|
|
109
|
+
merge-multiple: true
|
|
110
|
+
|
|
111
|
+
- name: Release
|
|
112
|
+
uses: softprops/action-gh-release@v1
|
|
113
|
+
with:
|
|
114
|
+
draft: true
|
|
115
|
+
body_path: ${{ github.workspace }}-CHANGELOG.txt
|
|
116
|
+
files: |
|
|
117
|
+
build/*
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
osam-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Kentaro Wada.
|
|
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.
|
osam-0.1.0/Makefile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
all:
|
|
2
|
+
@echo '## Make commands ##'
|
|
3
|
+
@echo
|
|
4
|
+
@$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | xargs
|
|
5
|
+
|
|
6
|
+
lint:
|
|
7
|
+
mypy --package osam
|
|
8
|
+
ruff format --check
|
|
9
|
+
ruff check
|
|
10
|
+
|
|
11
|
+
format:
|
|
12
|
+
ruff format
|
|
13
|
+
ruff check --fix
|
|
14
|
+
|
|
15
|
+
test:
|
|
16
|
+
python -m pytest -n auto -v tests
|
|
17
|
+
|
|
18
|
+
clean:
|
|
19
|
+
rm -rf build dist *.egg-info
|
|
20
|
+
|
|
21
|
+
build: clean
|
|
22
|
+
python -m build --sdist --wheel
|
|
23
|
+
|
|
24
|
+
upload: build
|
|
25
|
+
python -m twine upload dist/osam-*
|
|
26
|
+
|
|
27
|
+
publish: build upload
|
osam-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: osam
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Get up and running vision foundational models locally.
|
|
5
|
+
Project-URL: Homepage, https://github.com/wkentaro/osam
|
|
6
|
+
Author-email: Kentaro Wada <www.kentaro.wada@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Requires-Dist: click
|
|
24
|
+
Requires-Dist: fastapi
|
|
25
|
+
Requires-Dist: gdown>=5.1.0
|
|
26
|
+
Requires-Dist: loguru
|
|
27
|
+
Requires-Dist: onnxruntime
|
|
28
|
+
Requires-Dist: pillow
|
|
29
|
+
Requires-Dist: uvicorn
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: build; extra == 'test'
|
|
32
|
+
Requires-Dist: mypy; extra == 'test'
|
|
33
|
+
Requires-Dist: pytest; extra == 'test'
|
|
34
|
+
Requires-Dist: pytest-xdist; extra == 'test'
|
|
35
|
+
Requires-Dist: ruff; extra == 'test'
|
|
36
|
+
Requires-Dist: twine; extra == 'test'
|
|
37
|
+
Requires-Dist: types-pillow; extra == 'test'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
<div align="center">
|
|
41
|
+
<img alt="logo" height="200px" src=".readme/icon.png" >
|
|
42
|
+
<h1>Osam</h1>
|
|
43
|
+
<p>
|
|
44
|
+
Get up and running vision foundational models locally.
|
|
45
|
+
</p>
|
|
46
|
+
<br>
|
|
47
|
+
<br>
|
|
48
|
+
<br>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
*Osam* (/oʊˈsɑm/) is a tool to run open source vision foundational models locally,
|
|
52
|
+
built inspired by [Ollama](https://github.com/ollama/ollama).
|
|
53
|
+
|
|
54
|
+
*Osam* gives you:
|
|
55
|
+
|
|
56
|
+
- **Visual foundational models** - Segment-Anything Model, Efficient-SAM, etc;
|
|
57
|
+
- **Local APIs** - CLI & Python & HTTP interface;
|
|
58
|
+
- **Customization** - Host custom vision models.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install osam
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
### CLI
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Run a model with an image
|
|
74
|
+
osam run efficient-sam:25m --image examples/_images/dogs.jpg > output.jpg
|
|
75
|
+
|
|
76
|
+
# Get a JSON output
|
|
77
|
+
osam run efficient-sam:25m --image examples/_images/dogs.jpg --json
|
|
78
|
+
# {"model": "efficient-sam:25m", "mask": "..."}
|
|
79
|
+
|
|
80
|
+
# Give a prompt
|
|
81
|
+
osam run efficient-sam:25m --image examples/_images/dogs.jpg \
|
|
82
|
+
--prompt '{"points": [[1439, 504], [1439, 1289]], "point_labels": [1, 1]}' > output.jpg
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
<img src="examples/_images/dogs.jpg" width="35%"> <img src=".readme/dogs_output.jpg" width="35%">
|
|
86
|
+
<i>Input and output images ('dogs.jpg', 'output.jpg').</i>
|
|
87
|
+
|
|
88
|
+
### Python
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
import osam.apis
|
|
92
|
+
import osam.types
|
|
93
|
+
|
|
94
|
+
request = osam.types.GenerateRequest(
|
|
95
|
+
model="efficient-sam:25m",
|
|
96
|
+
image=np.asarray(PIL.Image.open("examples/_images/dogs.jpg")),
|
|
97
|
+
prompt=osam.types.Prompt(points=[[1439, 504], [1439, 1289]], point_labels=[1, 1]),
|
|
98
|
+
)
|
|
99
|
+
response = osam.apis.generate(request=request)
|
|
100
|
+
PIL.Image.fromarray(response.mask).save("mask.jpg")
|
|
101
|
+
```
|
|
102
|
+
<img src="examples/_images/dogs.jpg" width="35%"> <img src=".readme/dogs_mask.jpg" width="35%">
|
|
103
|
+
<i>Input and output images ('dogs.jpg', 'mask.jpg').</i>
|
|
104
|
+
|
|
105
|
+
### HTTP
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Get up the server
|
|
109
|
+
osam serve
|
|
110
|
+
|
|
111
|
+
# POST request
|
|
112
|
+
curl 127.0.0.1:11368/api/generate -X POST \
|
|
113
|
+
-H "Content-Type: application/json" \
|
|
114
|
+
-d "{\"model\": \"efficient-sam:25m\", \"image\": \"$(cat examples/_images/dogs.jpg | base64)\"}" \
|
|
115
|
+
| jq -r .mask | base64 --decode > mask.jpg
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
osam-0.1.0/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img alt="logo" height="200px" src=".readme/icon.png" >
|
|
3
|
+
<h1>Osam</h1>
|
|
4
|
+
<p>
|
|
5
|
+
Get up and running vision foundational models locally.
|
|
6
|
+
</p>
|
|
7
|
+
<br>
|
|
8
|
+
<br>
|
|
9
|
+
<br>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
*Osam* (/oʊˈsɑm/) is a tool to run open source vision foundational models locally,
|
|
13
|
+
built inspired by [Ollama](https://github.com/ollama/ollama).
|
|
14
|
+
|
|
15
|
+
*Osam* gives you:
|
|
16
|
+
|
|
17
|
+
- **Visual foundational models** - Segment-Anything Model, Efficient-SAM, etc;
|
|
18
|
+
- **Local APIs** - CLI & Python & HTTP interface;
|
|
19
|
+
- **Customization** - Host custom vision models.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install osam
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### CLI
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Run a model with an image
|
|
35
|
+
osam run efficient-sam:25m --image examples/_images/dogs.jpg > output.jpg
|
|
36
|
+
|
|
37
|
+
# Get a JSON output
|
|
38
|
+
osam run efficient-sam:25m --image examples/_images/dogs.jpg --json
|
|
39
|
+
# {"model": "efficient-sam:25m", "mask": "..."}
|
|
40
|
+
|
|
41
|
+
# Give a prompt
|
|
42
|
+
osam run efficient-sam:25m --image examples/_images/dogs.jpg \
|
|
43
|
+
--prompt '{"points": [[1439, 504], [1439, 1289]], "point_labels": [1, 1]}' > output.jpg
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
<img src="examples/_images/dogs.jpg" width="35%"> <img src=".readme/dogs_output.jpg" width="35%">
|
|
47
|
+
<i>Input and output images ('dogs.jpg', 'output.jpg').</i>
|
|
48
|
+
|
|
49
|
+
### Python
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import osam.apis
|
|
53
|
+
import osam.types
|
|
54
|
+
|
|
55
|
+
request = osam.types.GenerateRequest(
|
|
56
|
+
model="efficient-sam:25m",
|
|
57
|
+
image=np.asarray(PIL.Image.open("examples/_images/dogs.jpg")),
|
|
58
|
+
prompt=osam.types.Prompt(points=[[1439, 504], [1439, 1289]], point_labels=[1, 1]),
|
|
59
|
+
)
|
|
60
|
+
response = osam.apis.generate(request=request)
|
|
61
|
+
PIL.Image.fromarray(response.mask).save("mask.jpg")
|
|
62
|
+
```
|
|
63
|
+
<img src="examples/_images/dogs.jpg" width="35%"> <img src=".readme/dogs_mask.jpg" width="35%">
|
|
64
|
+
<i>Input and output images ('dogs.jpg', 'mask.jpg').</i>
|
|
65
|
+
|
|
66
|
+
### HTTP
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Get up the server
|
|
70
|
+
osam serve
|
|
71
|
+
|
|
72
|
+
# POST request
|
|
73
|
+
curl 127.0.0.1:11368/api/generate -X POST \
|
|
74
|
+
-H "Content-Type: application/json" \
|
|
75
|
+
-d "{\"model\": \"efficient-sam:25m\", \"image\": \"$(cat examples/_images/dogs.jpg | base64)\"}" \
|
|
76
|
+
| jq -r .mask | base64 --decode > mask.jpg
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
|
Binary file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
image_file='../_images/dogs.jpg'
|
|
4
|
+
number_of_choices=2
|
|
5
|
+
|
|
6
|
+
choices=$(osam list --all | awk '{print $1}' | sed '1d')
|
|
7
|
+
|
|
8
|
+
echo "Select $number_of_choices models to compare:"
|
|
9
|
+
select model in $choices; do
|
|
10
|
+
if [[ -n $model ]]; then
|
|
11
|
+
echo "You have selected $model"
|
|
12
|
+
selections+=("$model")
|
|
13
|
+
((COUNT++))
|
|
14
|
+
if [[ $COUNT -eq $number_of_choices ]]; then
|
|
15
|
+
break
|
|
16
|
+
fi
|
|
17
|
+
else
|
|
18
|
+
echo "Invalid selection"
|
|
19
|
+
fi
|
|
20
|
+
done
|
|
21
|
+
|
|
22
|
+
output_files=()
|
|
23
|
+
for model in "${selections[@]}"; do
|
|
24
|
+
echo "Running $model"
|
|
25
|
+
output_file=${model/:/-}.jpg
|
|
26
|
+
osam run $model --image $image_file > $output_file
|
|
27
|
+
echo "Exported $model.jpg"
|
|
28
|
+
output_files+=($output_file)
|
|
29
|
+
done
|
|
30
|
+
echo "Compare results: ${output_files[@]}"
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import io
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import numpy as np
|
|
9
|
+
import PIL.Image
|
|
10
|
+
import uvicorn
|
|
11
|
+
from loguru import logger
|
|
12
|
+
|
|
13
|
+
from osam import __version__
|
|
14
|
+
from osam import _humanize
|
|
15
|
+
from osam import _models
|
|
16
|
+
from osam import _tabulate
|
|
17
|
+
from osam import apis
|
|
18
|
+
from osam import types
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.group(context_settings=dict(help_option_names=["-h", "--help"]))
|
|
22
|
+
@click.version_option(__version__)
|
|
23
|
+
def cli():
|
|
24
|
+
logger.remove(0)
|
|
25
|
+
logger.add(
|
|
26
|
+
sys.stderr, level="INFO", colorize=True, format="<level>{message}</level>"
|
|
27
|
+
)
|
|
28
|
+
os.makedirs(os.path.expanduser("~/.cache/osam"), exist_ok=True)
|
|
29
|
+
logger.add(
|
|
30
|
+
os.path.expanduser("~/.cache/osam/osam.log"), colorize=True, level="DEBUG"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@cli.command(help="Help about any command")
|
|
35
|
+
@click.argument("subcommand", required=False, type=str)
|
|
36
|
+
@click.pass_context
|
|
37
|
+
def help(ctx, subcommand):
|
|
38
|
+
if subcommand is None:
|
|
39
|
+
click.echo(cli.get_help(ctx))
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
subcommand_obj = cli.get_command(ctx, subcommand)
|
|
43
|
+
if subcommand_obj is None:
|
|
44
|
+
logger.warning("Unknown subcommand {subcommand!r}", subcommand=subcommand)
|
|
45
|
+
click.echo(cli.get_help(ctx))
|
|
46
|
+
else:
|
|
47
|
+
click.echo(subcommand_obj.get_help(ctx))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@cli.command(help="List models")
|
|
51
|
+
@click.option("--all", "-a", "show_all", is_flag=True, help="show all models")
|
|
52
|
+
def list(show_all):
|
|
53
|
+
rows = []
|
|
54
|
+
for model in _models.MODELS:
|
|
55
|
+
size = model.get_size()
|
|
56
|
+
modified_at = model.get_modified_at()
|
|
57
|
+
|
|
58
|
+
if size is None or modified_at is None:
|
|
59
|
+
if show_all:
|
|
60
|
+
size = "<not pulled>"
|
|
61
|
+
modified_at = "<not pulled>"
|
|
62
|
+
else:
|
|
63
|
+
continue
|
|
64
|
+
else:
|
|
65
|
+
size = _humanize.naturalsize(size)
|
|
66
|
+
modified_at = _humanize.naturaltime(
|
|
67
|
+
datetime.datetime.fromtimestamp(modified_at)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
rows.append([model.name, model.get_id(), size, modified_at])
|
|
71
|
+
click.echo(_tabulate.tabulate(rows, headers=["NAME", "ID", "SIZE", "MODIFIED"]))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@cli.command(help="Pull a model")
|
|
75
|
+
@click.argument("model_name", metavar="model", type=str)
|
|
76
|
+
def pull(model_name):
|
|
77
|
+
for cls in _models.MODELS:
|
|
78
|
+
if cls.name == model_name:
|
|
79
|
+
break
|
|
80
|
+
else:
|
|
81
|
+
logger.warning("Model {model_name!r} not found.", model_name=model_name)
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
|
|
84
|
+
logger.info("Pulling {model_name!r}...", model_name=model_name)
|
|
85
|
+
cls.pull()
|
|
86
|
+
logger.info("Pulled {model_name!r}", model_name=model_name)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@cli.command(help="Remove a model")
|
|
90
|
+
@click.argument("model_name", metavar="model", type=str)
|
|
91
|
+
def rm(model_name):
|
|
92
|
+
for cls in _models.MODELS:
|
|
93
|
+
if cls.name == model_name:
|
|
94
|
+
break
|
|
95
|
+
else:
|
|
96
|
+
logger.warning("Model {model_name} not found.", model_name=model_name)
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
logger.info("Removing {model_name!r}...", model_name=model_name)
|
|
100
|
+
cls.remove()
|
|
101
|
+
logger.info("Removed {model_name!r}", model_name=model_name)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@cli.command(help="Start server")
|
|
105
|
+
@click.option("--reload", is_flag=True, help="reload server on file changes")
|
|
106
|
+
def serve(reload):
|
|
107
|
+
logger.info("Starting server...")
|
|
108
|
+
uvicorn.run("osam._server:app", host="127.0.0.1", port=11368, reload=reload)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@cli.command(help="Run a model")
|
|
112
|
+
@click.argument("model_name", metavar="model", type=str)
|
|
113
|
+
@click.option(
|
|
114
|
+
"--image",
|
|
115
|
+
"image_path",
|
|
116
|
+
type=click.Path(exists=True),
|
|
117
|
+
help="image path",
|
|
118
|
+
required=True,
|
|
119
|
+
)
|
|
120
|
+
@click.option("--prompt", type=json.loads, help="prompt")
|
|
121
|
+
@click.option("--json", is_flag=True, help="json output")
|
|
122
|
+
def run(model_name: str, image_path: str, prompt, json: bool) -> None:
|
|
123
|
+
try:
|
|
124
|
+
request: types.GenerateRequest = types.GenerateRequest(
|
|
125
|
+
model=model_name,
|
|
126
|
+
image=np.asarray(PIL.Image.open(image_path)),
|
|
127
|
+
prompt=prompt,
|
|
128
|
+
)
|
|
129
|
+
response: types.GenerateResponse = apis.generate(request=request)
|
|
130
|
+
except ValueError as e:
|
|
131
|
+
logger.error("{e}", e=e)
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
|
|
134
|
+
if json:
|
|
135
|
+
click.echo(response.model_dump_json())
|
|
136
|
+
else:
|
|
137
|
+
visualization: np.ndarray = (
|
|
138
|
+
0.5 * request.image
|
|
139
|
+
+ 0.5
|
|
140
|
+
* np.array([0, 255, 0])[None, None, :]
|
|
141
|
+
* (response.mask > 0)[:, :, None]
|
|
142
|
+
).astype(np.uint8)
|
|
143
|
+
sys.stdout.buffer.write(_image_ndarray_to_data(visualization))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _image_ndarray_to_data(ndarray: np.ndarray) -> bytes:
|
|
147
|
+
pil = PIL.Image.fromarray(ndarray)
|
|
148
|
+
with io.BytesIO() as f:
|
|
149
|
+
pil.save(f, format="PNG")
|
|
150
|
+
return f.getvalue()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
cli()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def naturalsize(size: int) -> str:
|
|
6
|
+
"""Convert byte size to human readable format."""
|
|
7
|
+
if size == 0:
|
|
8
|
+
return "0B"
|
|
9
|
+
base = 1024
|
|
10
|
+
prefixes = ["B", "KB", "MB", "GB", "TB", "PB"]
|
|
11
|
+
power = int(math.log(size, base))
|
|
12
|
+
prefix = prefixes[power]
|
|
13
|
+
size /= base**power
|
|
14
|
+
return f"{size:.2f} {prefix}"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def naturaltime(dt: datetime.datetime) -> str:
|
|
18
|
+
"""Convert datetime to human readable format."""
|
|
19
|
+
now = datetime.datetime.now()
|
|
20
|
+
delta = now - dt
|
|
21
|
+
if delta.days > 0:
|
|
22
|
+
return f"{delta.days} days ago"
|
|
23
|
+
elif delta.seconds < 60:
|
|
24
|
+
return f"{delta.seconds} seconds ago"
|
|
25
|
+
elif delta.seconds < 3600:
|
|
26
|
+
return f"{delta.seconds // 60} minutes ago"
|
|
27
|
+
else:
|
|
28
|
+
return f"{delta.seconds // 3600} hours ago"
|
osam-0.1.0/osam/_json.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import io
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import PIL.Image
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def image_ndarray_to_b64data(ndarray):
|
|
9
|
+
pil = PIL.Image.fromarray(ndarray)
|
|
10
|
+
f = io.BytesIO()
|
|
11
|
+
pil.save(f, format="PNG")
|
|
12
|
+
data = f.getvalue()
|
|
13
|
+
return base64.b64encode(data).decode("utf-8")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def image_b64data_to_ndarray(b64data):
|
|
17
|
+
data = base64.b64decode(b64data)
|
|
18
|
+
pil = PIL.Image.open(io.BytesIO(data))
|
|
19
|
+
return np.asarray(pil)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from ._base import ModelBase # noqa: F401
|
|
2
|
+
from ._efficient_sam import EfficientSam10m
|
|
3
|
+
from ._efficient_sam import EfficientSam25m
|
|
4
|
+
from ._sam import Sam91m
|
|
5
|
+
from ._sam import Sam308m
|
|
6
|
+
from ._sam import Sam636m
|
|
7
|
+
|
|
8
|
+
MODELS = [
|
|
9
|
+
EfficientSam10m,
|
|
10
|
+
EfficientSam25m,
|
|
11
|
+
Sam91m,
|
|
12
|
+
Sam308m,
|
|
13
|
+
Sam636m,
|
|
14
|
+
]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import hashlib
|
|
3
|
+
import os
|
|
4
|
+
from typing import Dict
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import gdown
|
|
8
|
+
import numpy as np
|
|
9
|
+
import onnxruntime
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
from osam import types
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclasses.dataclass
|
|
16
|
+
class ModelBlob:
|
|
17
|
+
url: str
|
|
18
|
+
hash: str
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def path(self):
|
|
22
|
+
return os.path.expanduser(f"~/.cache/osam/models/blobs/{self.hash}")
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def size(self):
|
|
26
|
+
if os.path.exists(self.path):
|
|
27
|
+
return os.stat(self.path).st_size
|
|
28
|
+
else:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def modified_at(self):
|
|
33
|
+
if os.path.exists(self.path):
|
|
34
|
+
return os.stat(self.path).st_mtime
|
|
35
|
+
else:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
def pull(self):
|
|
39
|
+
gdown.cached_download(url=self.url, path=self.path, hash=self.hash)
|
|
40
|
+
|
|
41
|
+
def remove(self):
|
|
42
|
+
if os.path.exists(self.path):
|
|
43
|
+
logger.debug("Removing model blob {path!r}", path=self.path)
|
|
44
|
+
os.remove(self.path)
|
|
45
|
+
else:
|
|
46
|
+
logger.warning("Model blob {path!r} not found", path=self.path)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ModelBase:
|
|
50
|
+
name: str
|
|
51
|
+
|
|
52
|
+
_blobs: Dict[str, ModelBlob]
|
|
53
|
+
_inference_sessions: Dict[str, onnxruntime.InferenceSession]
|
|
54
|
+
|
|
55
|
+
def __init__(self):
|
|
56
|
+
self.pull()
|
|
57
|
+
self._inference_sessions = {
|
|
58
|
+
key: onnxruntime.InferenceSession(blob.path)
|
|
59
|
+
for key, blob in self._blobs.items()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def pull(cls):
|
|
64
|
+
for blob in cls._blobs.values():
|
|
65
|
+
blob.pull()
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def remove(cls):
|
|
69
|
+
for blob in cls._blobs.values():
|
|
70
|
+
blob.remove()
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def get_id(cls) -> str:
|
|
74
|
+
return hashlib.md5(
|
|
75
|
+
"+".join(blob.hash for blob in cls._blobs.values()).encode()
|
|
76
|
+
).hexdigest()[:12]
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def get_size(cls) -> Optional[int]:
|
|
80
|
+
size = 0
|
|
81
|
+
for blob in cls._blobs.values():
|
|
82
|
+
if blob.size is None:
|
|
83
|
+
return None
|
|
84
|
+
size += blob.size
|
|
85
|
+
return size
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def get_modified_at(cls) -> Optional[float]:
|
|
89
|
+
modified_at = 0
|
|
90
|
+
for blob in cls._blobs.values():
|
|
91
|
+
if blob.modified_at is None:
|
|
92
|
+
return None
|
|
93
|
+
modified_at = max(modified_at, blob.modified_at)
|
|
94
|
+
return modified_at
|
|
95
|
+
|
|
96
|
+
def encode_image(self, image: np.ndarray) -> types.ImageEmbedding:
|
|
97
|
+
raise NotImplementedError
|
|
98
|
+
|
|
99
|
+
def generate_mask(
|
|
100
|
+
self, image_embedding: types.ImageEmbedding, prompt: types.Prompt
|
|
101
|
+
) -> np.ndarray:
|
|
102
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from osam._models._base import ModelBase
|
|
4
|
+
from osam._models._base import ModelBlob
|
|
5
|
+
from osam.types import ImageEmbedding
|
|
6
|
+
from osam.types import Prompt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EfficientSam(ModelBase):
|
|
10
|
+
def encode_image(self, image: np.ndarray) -> ImageEmbedding:
|
|
11
|
+
if image.ndim == 2:
|
|
12
|
+
raise ValueError("Grayscale images are not supported")
|
|
13
|
+
if image.ndim == 3 and image.shape[2] == 4:
|
|
14
|
+
raise ValueError("RGBA images are not supported")
|
|
15
|
+
|
|
16
|
+
batched_images = image.transpose(2, 0, 1)[None].astype(np.float32) / 255
|
|
17
|
+
image_embedding = self._inference_sessions["encoder"].run(
|
|
18
|
+
output_names=None,
|
|
19
|
+
input_feed={"batched_images": batched_images},
|
|
20
|
+
)[0][0] # (embedding_dim, height, width)
|
|
21
|
+
|
|
22
|
+
return ImageEmbedding(
|
|
23
|
+
original_height=image.shape[0],
|
|
24
|
+
original_width=image.shape[1],
|
|
25
|
+
embedding=image_embedding,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def generate_mask(
|
|
29
|
+
self,
|
|
30
|
+
image_embedding: ImageEmbedding,
|
|
31
|
+
prompt: Prompt,
|
|
32
|
+
) -> np.ndarray:
|
|
33
|
+
input_point = np.array(prompt.points, dtype=np.float32)
|
|
34
|
+
input_label = np.array(prompt.point_labels, dtype=np.float32)
|
|
35
|
+
|
|
36
|
+
# batch_size, embedding_dim, height, width
|
|
37
|
+
batched_image_embedding = image_embedding.embedding[None, :, :, :]
|
|
38
|
+
# batch_size, num_queries, num_points, 2
|
|
39
|
+
batched_point_coords = input_point[None, None, :, :]
|
|
40
|
+
# batch_size, num_queries, num_points
|
|
41
|
+
batched_point_labels = input_label[None, None, :]
|
|
42
|
+
|
|
43
|
+
decoder_inputs = {
|
|
44
|
+
"image_embeddings": batched_image_embedding,
|
|
45
|
+
"batched_point_coords": batched_point_coords,
|
|
46
|
+
"batched_point_labels": batched_point_labels,
|
|
47
|
+
"orig_im_size": np.array(
|
|
48
|
+
(image_embedding.original_height, image_embedding.original_width),
|
|
49
|
+
dtype=np.int64,
|
|
50
|
+
),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
masks, _, _ = self._inference_sessions["decoder"].run(None, decoder_inputs)
|
|
54
|
+
mask = masks[0, 0, 0, :, :] # (1, 1, 3, H, W) -> (H, W)
|
|
55
|
+
mask = mask > 0.0
|
|
56
|
+
|
|
57
|
+
return mask
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class EfficientSam10m(EfficientSam):
|
|
61
|
+
name = "efficient-sam:10m"
|
|
62
|
+
|
|
63
|
+
_blobs = {
|
|
64
|
+
"encoder": ModelBlob(
|
|
65
|
+
url="https://github.com/labelmeai/efficient-sam/releases/download/onnx-models-20231225/efficient_sam_vitt_encoder.onnx",
|
|
66
|
+
hash="sha256:7a73ee65aa2c37237c89b4b18e73082f757ffb173899609c5d97a2bbd4ebb02d",
|
|
67
|
+
),
|
|
68
|
+
"decoder": ModelBlob(
|
|
69
|
+
url="https://github.com/labelmeai/efficient-sam/releases/download/onnx-models-20231225/efficient_sam_vitt_decoder.onnx",
|
|
70
|
+
hash="sha256:e1afe46232c3bfa3470a6a81c7d3181836a94ea89528aff4e0f2d2c611989efd",
|
|
71
|
+
),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class EfficientSam25m(EfficientSam):
|
|
76
|
+
name = "efficient-sam:25m"
|
|
77
|
+
|
|
78
|
+
_blobs = {
|
|
79
|
+
"encoder": ModelBlob(
|
|
80
|
+
url="https://github.com/labelmeai/efficient-sam/releases/download/onnx-models-20231225/efficient_sam_vits_encoder.onnx",
|
|
81
|
+
hash="sha256:4cacbb23c6903b1acf87f1d77ed806b840800c5fcd4ac8f650cbffed474b8896",
|
|
82
|
+
),
|
|
83
|
+
"decoder": ModelBlob(
|
|
84
|
+
url="https://github.com/labelmeai/efficient-sam/releases/download/onnx-models-20231225/efficient_sam_vits_decoder.onnx",
|
|
85
|
+
hash="sha256:4727baf23dacfb51d4c16795b2ac382c403505556d0284e84c6ff3d4e8e36f22",
|
|
86
|
+
),
|
|
87
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import onnxruntime
|
|
3
|
+
import PIL.Image
|
|
4
|
+
|
|
5
|
+
from osam._models._base import ModelBase
|
|
6
|
+
from osam._models._base import ModelBlob
|
|
7
|
+
from osam.types import ImageEmbedding
|
|
8
|
+
from osam.types import Prompt
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Sam(ModelBase):
|
|
12
|
+
_image_size: int = 1024
|
|
13
|
+
|
|
14
|
+
def encode_image(self, image: np.ndarray):
|
|
15
|
+
if image.ndim == 2:
|
|
16
|
+
raise ValueError("Grayscale images are not supported")
|
|
17
|
+
if image.ndim == 3 and image.shape[2] == 4:
|
|
18
|
+
raise ValueError("RGBA images are not supported")
|
|
19
|
+
|
|
20
|
+
image_embedding = _compute_image_embedding(
|
|
21
|
+
encoder_session=self._inference_sessions["encoder"],
|
|
22
|
+
image=image,
|
|
23
|
+
image_size=self._image_size,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return ImageEmbedding(
|
|
27
|
+
original_height=image.shape[0],
|
|
28
|
+
original_width=image.shape[1],
|
|
29
|
+
embedding=image_embedding,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def generate_mask(
|
|
33
|
+
self,
|
|
34
|
+
image_embedding: ImageEmbedding,
|
|
35
|
+
prompt: Prompt,
|
|
36
|
+
) -> np.ndarray:
|
|
37
|
+
return _generate_mask(
|
|
38
|
+
decoder_session=self._inference_sessions["decoder"],
|
|
39
|
+
image_embedding=image_embedding,
|
|
40
|
+
prompt=prompt,
|
|
41
|
+
image_size=self._image_size,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _compute_scale_to_resize_image(height: int, width: int, image_size: int):
|
|
46
|
+
if width > height:
|
|
47
|
+
scale = image_size / width
|
|
48
|
+
new_height = int(round(height * scale))
|
|
49
|
+
new_width = image_size
|
|
50
|
+
else:
|
|
51
|
+
scale = image_size / height
|
|
52
|
+
new_height = image_size
|
|
53
|
+
new_width = int(round(width * scale))
|
|
54
|
+
return scale, new_height, new_width
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _resize_image(image: np.ndarray, image_size: int):
|
|
58
|
+
scale, new_height, new_width = _compute_scale_to_resize_image(
|
|
59
|
+
height=image.shape[0],
|
|
60
|
+
width=image.shape[1],
|
|
61
|
+
image_size=image_size,
|
|
62
|
+
)
|
|
63
|
+
scaled_image = np.asarray(
|
|
64
|
+
PIL.Image.fromarray(image).resize(
|
|
65
|
+
(new_width, new_height), resample=PIL.Image.BILINEAR
|
|
66
|
+
)
|
|
67
|
+
).astype(np.float32)
|
|
68
|
+
return scale, scaled_image
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _compute_image_embedding(
|
|
72
|
+
encoder_session: onnxruntime.InferenceSession,
|
|
73
|
+
image: np.ndarray,
|
|
74
|
+
image_size: int,
|
|
75
|
+
):
|
|
76
|
+
scale, x = _resize_image(image=image, image_size=image_size)
|
|
77
|
+
x = (x - np.array([123.675, 116.28, 103.53], dtype=np.float32)) / np.array(
|
|
78
|
+
[58.395, 57.12, 57.375], dtype=np.float32
|
|
79
|
+
)
|
|
80
|
+
x = np.pad(
|
|
81
|
+
x,
|
|
82
|
+
(
|
|
83
|
+
(0, image_size - x.shape[0]),
|
|
84
|
+
(0, image_size - x.shape[1]),
|
|
85
|
+
(0, 0),
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
x = x.transpose(2, 0, 1)[None, :, :, :]
|
|
89
|
+
|
|
90
|
+
output = encoder_session.run(output_names=None, input_feed={"x": x})
|
|
91
|
+
image_embedding = output[0][0] # (embedding_dim, height, width)
|
|
92
|
+
return image_embedding
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _generate_mask(
|
|
96
|
+
decoder_session: onnxruntime.InferenceSession,
|
|
97
|
+
image_embedding: ImageEmbedding,
|
|
98
|
+
prompt: Prompt,
|
|
99
|
+
image_size: int,
|
|
100
|
+
):
|
|
101
|
+
onnx_coord = np.concatenate([prompt.points, np.array([[0.0, 0.0]])], axis=0)[
|
|
102
|
+
None, :, :
|
|
103
|
+
]
|
|
104
|
+
onnx_label = np.concatenate([prompt.point_labels, np.array([-1])], axis=0)[
|
|
105
|
+
None, :
|
|
106
|
+
].astype(np.float32)
|
|
107
|
+
|
|
108
|
+
scale, new_height, new_width = _compute_scale_to_resize_image(
|
|
109
|
+
height=image_embedding.original_height,
|
|
110
|
+
width=image_embedding.original_width,
|
|
111
|
+
image_size=image_size,
|
|
112
|
+
)
|
|
113
|
+
onnx_coord = (
|
|
114
|
+
onnx_coord.astype(float)
|
|
115
|
+
* (
|
|
116
|
+
new_width / image_embedding.original_width,
|
|
117
|
+
new_height / image_embedding.original_height,
|
|
118
|
+
)
|
|
119
|
+
).astype(np.float32)
|
|
120
|
+
|
|
121
|
+
onnx_mask_input = np.zeros((1, 1, 256, 256), dtype=np.float32)
|
|
122
|
+
onnx_has_mask_input = np.array([-1], dtype=np.float32)
|
|
123
|
+
|
|
124
|
+
decoder_inputs = {
|
|
125
|
+
"image_embeddings": image_embedding.embedding[None, :, :, :],
|
|
126
|
+
"point_coords": onnx_coord,
|
|
127
|
+
"point_labels": onnx_label,
|
|
128
|
+
"mask_input": onnx_mask_input,
|
|
129
|
+
"has_mask_input": onnx_has_mask_input,
|
|
130
|
+
"orig_im_size": np.array(
|
|
131
|
+
(image_embedding.original_height, image_embedding.original_width),
|
|
132
|
+
dtype=np.float32,
|
|
133
|
+
),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
masks, _, _ = decoder_session.run(None, decoder_inputs)
|
|
137
|
+
mask = masks[0, 0] # (1, 1, H, W) -> (H, W)
|
|
138
|
+
mask = mask > 0.0
|
|
139
|
+
return mask
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Sam91m(Sam):
|
|
143
|
+
name = "sam:91m"
|
|
144
|
+
|
|
145
|
+
_blobs = {
|
|
146
|
+
"encoder": ModelBlob(
|
|
147
|
+
url="https://github.com/wkentaro/labelme/releases/download/sam-20230416/sam_vit_b_01ec64.quantized.encoder.onnx",
|
|
148
|
+
hash="sha256:3346b9cc551c9902fbf3b203935e933592b22e042365f58321c17fc12641fd6a",
|
|
149
|
+
),
|
|
150
|
+
"decoder": ModelBlob(
|
|
151
|
+
url="https://github.com/wkentaro/labelme/releases/download/sam-20230416/sam_vit_b_01ec64.quantized.decoder.onnx",
|
|
152
|
+
hash="sha256:edbcf1a0afaa55621fb0abe6b3db1516818b609ea9368f309746a3afc68f7613",
|
|
153
|
+
),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class Sam308m(Sam):
|
|
158
|
+
name = "sam:308m"
|
|
159
|
+
|
|
160
|
+
_blobs = {
|
|
161
|
+
"encoder": ModelBlob(
|
|
162
|
+
url="https://github.com/wkentaro/labelme/releases/download/sam-20230416/sam_vit_l_0b3195.quantized.encoder.onnx",
|
|
163
|
+
hash="sha256:f7158a4a1fe7f670ef47ea2f7f852685425c1ed6caa40f5df86cbe2b0502034f",
|
|
164
|
+
),
|
|
165
|
+
"decoder": ModelBlob(
|
|
166
|
+
url="https://github.com/wkentaro/labelme/releases/download/sam-20230416/sam_vit_l_0b3195.quantized.decoder.onnx",
|
|
167
|
+
hash="sha256:552ebb23bf52c5e5b971ac710d1eb8dccfd88b36cc6aff881d1536d1662e6d7b",
|
|
168
|
+
),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class Sam636m(Sam):
|
|
173
|
+
name = "sam:636m"
|
|
174
|
+
|
|
175
|
+
_blobs = {
|
|
176
|
+
"encoder": ModelBlob(
|
|
177
|
+
url="https://github.com/wkentaro/labelme/releases/download/sam-20230416/sam_vit_h_4b8939.quantized.encoder.onnx",
|
|
178
|
+
hash="sha256:a5c745fd4279efc5e5436b412200e983dafc2249ce172af6cc0002a71bb5f485",
|
|
179
|
+
),
|
|
180
|
+
"decoder": ModelBlob(
|
|
181
|
+
url="https://github.com/wkentaro/labelme/releases/download/sam-20230416/sam_vit_h_4b8939.quantized.decoder.onnx",
|
|
182
|
+
hash="sha256:020b385a45ffe51097e1acd10cd791075a86171153505f789a793bc382eef210",
|
|
183
|
+
),
|
|
184
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import fastapi
|
|
5
|
+
|
|
6
|
+
from osam import apis
|
|
7
|
+
from osam import types
|
|
8
|
+
|
|
9
|
+
app: fastapi.FastAPI = fastapi.FastAPI()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.get("/")
|
|
13
|
+
async def index():
|
|
14
|
+
return fastapi.Response("osam is running")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.post("/api/generate")
|
|
18
|
+
async def generate(request: types.GenerateRequest) -> types.GenerateResponse:
|
|
19
|
+
try:
|
|
20
|
+
return apis.generate(request=request)
|
|
21
|
+
except ValueError as e:
|
|
22
|
+
raise fastapi.HTTPException(400, str(e))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# https://github.com/tiangolo/fastapi/issues/1508#issuecomment-638365277
|
|
26
|
+
@app.on_event("startup")
|
|
27
|
+
async def startup_event():
|
|
28
|
+
uvicorn_logger = logging.getLogger("uvicorn")
|
|
29
|
+
handler = logging.FileHandler(os.path.expanduser("~/.cache/osam/osam.log"))
|
|
30
|
+
handler.setLevel(logging.DEBUG)
|
|
31
|
+
fmt = "%(asctime)s [%(levelname)s] %(module)s:%(funcName)s:%(lineno)s - %(message)s"
|
|
32
|
+
handler.setFormatter(logging.Formatter(fmt=fmt))
|
|
33
|
+
uvicorn_logger.addHandler(handler)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def tabulate(rows, headers):
|
|
5
|
+
col_widths = collections.defaultdict(int)
|
|
6
|
+
for row in [headers] + rows:
|
|
7
|
+
for c, item in enumerate(row):
|
|
8
|
+
col_widths[c] = max(col_widths[c], len(item))
|
|
9
|
+
|
|
10
|
+
table = ""
|
|
11
|
+
|
|
12
|
+
for c, header in enumerate(headers):
|
|
13
|
+
table += header.ljust(col_widths[c] + 4)
|
|
14
|
+
|
|
15
|
+
for row in rows:
|
|
16
|
+
table += "\n"
|
|
17
|
+
for c, item in enumerate(row):
|
|
18
|
+
table += item.ljust(col_widths[c] + 4)
|
|
19
|
+
return table
|
osam-0.1.0/osam/apis.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from loguru import logger
|
|
5
|
+
|
|
6
|
+
from osam import _models
|
|
7
|
+
from osam import types
|
|
8
|
+
|
|
9
|
+
model: Optional[_models.ModelBase] = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def generate(request: types.GenerateRequest) -> types.GenerateResponse:
|
|
13
|
+
global model
|
|
14
|
+
|
|
15
|
+
if model is None or model.name != request.model:
|
|
16
|
+
for model_cls in _models.MODELS:
|
|
17
|
+
if model_cls.name == request.model:
|
|
18
|
+
model = model_cls()
|
|
19
|
+
break
|
|
20
|
+
else:
|
|
21
|
+
raise ValueError(f"Model not found: {request.model!r}")
|
|
22
|
+
|
|
23
|
+
image: np.ndarray = request.image
|
|
24
|
+
|
|
25
|
+
if request.prompt is None:
|
|
26
|
+
height, width = image.shape[:2]
|
|
27
|
+
request.prompt = types.Prompt(
|
|
28
|
+
points=np.array([[width / 2, height / 2]], dtype=np.float32),
|
|
29
|
+
point_labels=np.array([1], dtype=np.int32),
|
|
30
|
+
)
|
|
31
|
+
logger.warning(
|
|
32
|
+
"Prompt is not given, so using the center point as prompt: {prompt!r}",
|
|
33
|
+
prompt=request.prompt,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
image_embedding: types.ImageEmbedding = model.encode_image(image=image)
|
|
37
|
+
mask: np.ndarray = model.generate_mask(
|
|
38
|
+
image_embedding=image_embedding, prompt=request.prompt
|
|
39
|
+
)
|
|
40
|
+
return types.GenerateResponse(model=request.model, mask=mask)
|
osam-0.1.0/osam/types.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pydantic
|
|
7
|
+
|
|
8
|
+
from osam import _json
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ImageEmbedding(pydantic.BaseModel):
|
|
12
|
+
model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
|
|
13
|
+
|
|
14
|
+
original_height: int
|
|
15
|
+
original_width: int
|
|
16
|
+
embedding: np.ndarray
|
|
17
|
+
|
|
18
|
+
@pydantic.validator("embedding")
|
|
19
|
+
def validate_embedding(cls, embedding):
|
|
20
|
+
if embedding.ndim != 3:
|
|
21
|
+
raise ValueError(
|
|
22
|
+
"embedding must be 3-dimensional: (embedding_dim, height, width)"
|
|
23
|
+
)
|
|
24
|
+
return embedding
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Prompt(pydantic.BaseModel):
|
|
28
|
+
model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
|
|
29
|
+
|
|
30
|
+
points: np.ndarray
|
|
31
|
+
point_labels: np.ndarray
|
|
32
|
+
|
|
33
|
+
@pydantic.validator("points", pre=True)
|
|
34
|
+
def validate_points(cls, points):
|
|
35
|
+
if isinstance(points, list):
|
|
36
|
+
points = np.array(points, dtype=float)
|
|
37
|
+
if points.ndim != 2:
|
|
38
|
+
raise ValueError("points must be 2-dimensional")
|
|
39
|
+
if points.shape[1] != 2:
|
|
40
|
+
raise ValueError("points must have 2 columns")
|
|
41
|
+
return points
|
|
42
|
+
|
|
43
|
+
@pydantic.field_serializer("points")
|
|
44
|
+
def serialize_points(self, points: np.ndarray) -> List[List[float]]:
|
|
45
|
+
return points.tolist()
|
|
46
|
+
|
|
47
|
+
@pydantic.validator("point_labels", pre=True)
|
|
48
|
+
def validate_point_labels(cls, point_labels, values):
|
|
49
|
+
if isinstance(point_labels, list):
|
|
50
|
+
point_labels = np.array(point_labels, dtype=int)
|
|
51
|
+
if point_labels.ndim != 1:
|
|
52
|
+
raise ValueError("point_labels must be 1-dimensional")
|
|
53
|
+
if "points" in values and point_labels.shape[0] != values["points"].shape[0]:
|
|
54
|
+
raise ValueError("point_labels must have the same number of rows as points")
|
|
55
|
+
if not set(np.unique(point_labels).tolist()).issubset({0, 1}):
|
|
56
|
+
raise ValueError("point_labels must contain only 0s and 1s")
|
|
57
|
+
return point_labels
|
|
58
|
+
|
|
59
|
+
@pydantic.field_serializer("point_labels")
|
|
60
|
+
def serialize_point_labels(self, point_labels: np.ndarray) -> List[int]:
|
|
61
|
+
return point_labels.tolist()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class GenerateRequest(pydantic.BaseModel):
|
|
65
|
+
model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
|
|
66
|
+
|
|
67
|
+
model: str
|
|
68
|
+
image: np.ndarray
|
|
69
|
+
prompt: Optional[Prompt] = pydantic.Field(default=None)
|
|
70
|
+
|
|
71
|
+
@pydantic.validator("image", pre=True)
|
|
72
|
+
def validate_image(cls, image: Union[str, np.ndarray]) -> np.ndarray:
|
|
73
|
+
if isinstance(image, str):
|
|
74
|
+
return _json.image_b64data_to_ndarray(b64data=image)
|
|
75
|
+
return image
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class GenerateResponse(pydantic.BaseModel):
|
|
79
|
+
model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
|
|
80
|
+
|
|
81
|
+
model: str
|
|
82
|
+
mask: np.ndarray
|
|
83
|
+
|
|
84
|
+
@pydantic.field_serializer("mask")
|
|
85
|
+
def serialize_mask(self, mask: np.ndarray) -> str:
|
|
86
|
+
return _json.image_ndarray_to_b64data(ndarray=mask)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.20.0", "hatch-vcs", "hatch-fancy-pypi-readme"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "osam"
|
|
7
|
+
description = "Get up and running vision foundational models locally."
|
|
8
|
+
license = { text = "MIT" }
|
|
9
|
+
requires-python = ">=3.8"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Kentaro Wada", email = "www.kentaro.wada@gmail.com" },
|
|
12
|
+
]
|
|
13
|
+
keywords = []
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.8",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"click",
|
|
31
|
+
"gdown>=5.1.0",
|
|
32
|
+
"fastapi",
|
|
33
|
+
"loguru",
|
|
34
|
+
"onnxruntime",
|
|
35
|
+
"Pillow",
|
|
36
|
+
"uvicorn",
|
|
37
|
+
]
|
|
38
|
+
dynamic = ["readme", "version"]
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
test = [
|
|
42
|
+
"build",
|
|
43
|
+
"mypy",
|
|
44
|
+
"types-Pillow",
|
|
45
|
+
"pytest",
|
|
46
|
+
"pytest-xdist",
|
|
47
|
+
"ruff",
|
|
48
|
+
"twine",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[project.urls]
|
|
52
|
+
Homepage = "https://github.com/wkentaro/osam"
|
|
53
|
+
|
|
54
|
+
[tool.hatch.metadata.hooks.fancy-pypi-readme]
|
|
55
|
+
content-type = "text/markdown"
|
|
56
|
+
fragments = [
|
|
57
|
+
{ path = "README.md" },
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[tool.hatch.version]
|
|
61
|
+
source = "vcs"
|
|
62
|
+
|
|
63
|
+
[project.scripts]
|
|
64
|
+
osam = "osam:__main__.cli"
|
|
65
|
+
|
|
66
|
+
[tool.mypy]
|
|
67
|
+
ignore_missing_imports = true
|
|
68
|
+
|
|
69
|
+
[tool.ruff]
|
|
70
|
+
exclude = [
|
|
71
|
+
".conda",
|
|
72
|
+
".git",
|
|
73
|
+
"src",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
line-length = 88
|
|
77
|
+
indent-width = 4
|
|
78
|
+
|
|
79
|
+
[tool.ruff.lint]
|
|
80
|
+
# Enable Pyflakes (`F`), pycodestyle (`E`), isort (`I`).
|
|
81
|
+
select = ["E", "F", "I"]
|
|
82
|
+
ignore = []
|
|
83
|
+
|
|
84
|
+
# Allow fix for all enabled rules (when `--fix`) is provided.
|
|
85
|
+
fixable = ["ALL"]
|
|
86
|
+
unfixable = []
|
|
87
|
+
|
|
88
|
+
[tool.ruff.format]
|
|
89
|
+
# Like Black, use double quotes for strings.
|
|
90
|
+
quote-style = "double"
|
|
91
|
+
|
|
92
|
+
# Like Black, indent with spaces, rather than tabs.
|
|
93
|
+
indent-style = "space"
|
|
94
|
+
|
|
95
|
+
# Like Black, respect magic trailing commas.
|
|
96
|
+
skip-magic-trailing-comma = false
|
|
97
|
+
|
|
98
|
+
# Like Black, automatically detect the appropriate line ending.
|
|
99
|
+
line-ending = "auto"
|
|
100
|
+
|
|
101
|
+
[tool.ruff.isort]
|
|
102
|
+
force-single-line = true
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
import shlex
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
import PIL.Image
|
|
7
|
+
|
|
8
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_run():
|
|
12
|
+
cmd = f"osam run efficient-sam:10m --image {here}/../examples/_images/dogs.jpg"
|
|
13
|
+
output = subprocess.check_output(shlex.split(cmd))
|
|
14
|
+
image = PIL.Image.open(io.BytesIO(output))
|
|
15
|
+
assert image.size == (2560, 1600)
|