Upload 30 files
Browse files- .dockerignore +47 -0
- .gitattributes +37 -35
- .gitignore +46 -0
- DOCKER.md +147 -0
- Dockerfile +71 -0
- README.md +418 -12
- analyze_semantic_failures.py +43 -0
- app.py +1058 -0
- data/CoGenT_A.json +11 -0
- data/CoGenT_B.json +11 -0
- data/base_scene.blend +3 -0
- data/materials/MyMetal.blend +3 -0
- data/materials/Rubber.blend +3 -0
- data/properties.json +25 -0
- data/shapes/SmoothCube_v2.blend +3 -0
- data/shapes/SmoothCylinder.blend +3 -0
- data/shapes/Sphere.blend +3 -0
- docker-compose.yml +26 -0
- generate_semantic_semibalanced_200.py +63 -0
- pipeline.py +2200 -0
- requirements.txt +12 -0
- scripts/README.md +107 -0
- scripts/__pycache__/generate_examples.cpython-312.pyc +0 -0
- scripts/__pycache__/generate_questions_mapping.cpython-312.pyc +3 -0
- scripts/__pycache__/generate_scenes.cpython-312.pyc +0 -0
- scripts/__pycache__/render.cpython-312.pyc +0 -0
- scripts/generate_examples.py +218 -0
- scripts/generate_questions_mapping.py +0 -0
- scripts/generate_scenes.py +242 -0
- scripts/render.py +1204 -0
.dockerignore
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
*.egg-info/
|
| 8 |
+
dist/
|
| 9 |
+
build/
|
| 10 |
+
|
| 11 |
+
# Virtual environments
|
| 12 |
+
venv/
|
| 13 |
+
env/
|
| 14 |
+
ENV/
|
| 15 |
+
|
| 16 |
+
# IDE
|
| 17 |
+
.vscode/
|
| 18 |
+
.idea/
|
| 19 |
+
*.swp
|
| 20 |
+
*.swo
|
| 21 |
+
|
| 22 |
+
# Git
|
| 23 |
+
.git/
|
| 24 |
+
.gitignore
|
| 25 |
+
|
| 26 |
+
# Output directories (will be mounted as volumes)
|
| 27 |
+
output/
|
| 28 |
+
temp_output/
|
| 29 |
+
|
| 30 |
+
# Documentation
|
| 31 |
+
*.md
|
| 32 |
+
README.md
|
| 33 |
+
DOCKER.md
|
| 34 |
+
|
| 35 |
+
# Docker files (don't need in image)
|
| 36 |
+
docker-compose.yml
|
| 37 |
+
Dockerfile
|
| 38 |
+
.dockerignore
|
| 39 |
+
|
| 40 |
+
# Large data files - NOTE: We NEED these for Blender to work
|
| 41 |
+
# *.blend
|
| 42 |
+
# data/shapes/
|
| 43 |
+
# data/materials/
|
| 44 |
+
|
| 45 |
+
# OS files
|
| 46 |
+
.DS_Store
|
| 47 |
+
Thumbs.db
|
.gitattributes
CHANGED
|
@@ -1,35 +1,37 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.blend filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
scripts/__pycache__/generate_questions_mapping.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
env/
|
| 8 |
+
venv/
|
| 9 |
+
ENV/
|
| 10 |
+
build/
|
| 11 |
+
develop-eggs/
|
| 12 |
+
dist/
|
| 13 |
+
downloads/
|
| 14 |
+
eggs/
|
| 15 |
+
.eggs/
|
| 16 |
+
lib/
|
| 17 |
+
lib64/
|
| 18 |
+
parts/
|
| 19 |
+
sdist/
|
| 20 |
+
var/
|
| 21 |
+
wheels/
|
| 22 |
+
*.egg-info/
|
| 23 |
+
.installed.cfg
|
| 24 |
+
*.egg
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# Output directory (generated scenes and images)
|
| 28 |
+
output/
|
| 29 |
+
|
| 30 |
+
# Blender temp files
|
| 31 |
+
temp_output/
|
| 32 |
+
temp_scenes/
|
| 33 |
+
render_images_patched.py
|
| 34 |
+
render_patched.py
|
| 35 |
+
*.blend1
|
| 36 |
+
|
| 37 |
+
# IDE
|
| 38 |
+
.vscode/
|
| 39 |
+
.idea/
|
| 40 |
+
*.swp
|
| 41 |
+
*.swo
|
| 42 |
+
*~
|
| 43 |
+
|
| 44 |
+
# OS
|
| 45 |
+
.DS_Store
|
| 46 |
+
Thumbs.db
|
DOCKER.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Docker Setup for Counterfactual Image Generator
|
| 2 |
+
|
| 3 |
+
This project includes Docker support for easy deployment and consistent environments.
|
| 4 |
+
|
| 5 |
+
**Windows Note:** This Dockerfile uses a Linux container (Ubuntu), which works perfectly on Windows. Docker Desktop on Windows runs Linux containers by default. The containerized environment is Linux, so Windows-specific code paths in the application won't apply inside the container.
|
| 6 |
+
|
| 7 |
+
## Building the Docker Image
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
docker build -t counterfactual-generator .
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
## Running with Docker
|
| 14 |
+
|
| 15 |
+
### Basic Usage
|
| 16 |
+
|
| 17 |
+
**Linux/macOS:**
|
| 18 |
+
```bash
|
| 19 |
+
# Run with default command (shows help)
|
| 20 |
+
docker run --rm counterfactual-generator
|
| 21 |
+
|
| 22 |
+
# Generate 10 scenes with 5 objects each
|
| 23 |
+
docker run --rm -v $(pwd)/output:/app/output counterfactual-generator \
|
| 24 |
+
python3 pipeline.py --num_scenes 10 --num_objects 5 --run_name docker_test
|
| 25 |
+
|
| 26 |
+
# With GPU support (requires nvidia-docker)
|
| 27 |
+
docker run --rm --gpus all -v $(pwd)/output:/app/output counterfactual-generator \
|
| 28 |
+
python3 pipeline.py --num_scenes 10 --num_objects 5 --use_gpu 1
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
**Windows (PowerShell):**
|
| 32 |
+
```powershell
|
| 33 |
+
# Run with default command (shows help)
|
| 34 |
+
docker run --rm counterfactual-generator
|
| 35 |
+
|
| 36 |
+
# Generate 10 scenes with 5 objects each
|
| 37 |
+
docker run --rm -v ${PWD}/output:/app/output counterfactual-generator `
|
| 38 |
+
python3 pipeline.py --num_scenes 10 --num_objects 5 --run_name docker_test
|
| 39 |
+
|
| 40 |
+
# With GPU support (requires WSL2 and nvidia-docker)
|
| 41 |
+
docker run --rm --gpus all -v ${PWD}/output:/app/output counterfactual-generator `
|
| 42 |
+
python3 pipeline.py --num_scenes 10 --num_objects 5 --use_gpu 1
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
**Windows (Command Prompt):**
|
| 46 |
+
```cmd
|
| 47 |
+
docker run --rm -v "%CD%/output:/app/output" counterfactual-generator ^
|
| 48 |
+
python3 pipeline.py --num_scenes 10 --num_objects 5 --run_name docker_test
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
### Using Docker Compose
|
| 52 |
+
|
| 53 |
+
```bash
|
| 54 |
+
# Build and run with docker-compose
|
| 55 |
+
docker-compose up
|
| 56 |
+
|
| 57 |
+
# Run in detached mode
|
| 58 |
+
docker-compose up -d
|
| 59 |
+
|
| 60 |
+
# View logs
|
| 61 |
+
docker-compose logs -f
|
| 62 |
+
|
| 63 |
+
# Stop the container
|
| 64 |
+
docker-compose down
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
### Customize the Command
|
| 68 |
+
|
| 69 |
+
Edit `docker-compose.yml` to change the default command:
|
| 70 |
+
|
| 71 |
+
```yaml
|
| 72 |
+
command: python3 pipeline.py --num_scenes 10 --num_objects 5 --use_gpu 1 --run_name my_experiment
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
## Volume Mounting
|
| 76 |
+
|
| 77 |
+
The output directory is mounted to `./output` on your host machine, so generated scenes and images will persist after the container stops.
|
| 78 |
+
|
| 79 |
+
## GPU Support
|
| 80 |
+
|
| 81 |
+
For GPU rendering support, ensure you have:
|
| 82 |
+
1. NVIDIA Docker runtime installed
|
| 83 |
+
2. Use `--gpus all` flag with `docker run` or add to `docker-compose.yml`:
|
| 84 |
+
|
| 85 |
+
```yaml
|
| 86 |
+
deploy:
|
| 87 |
+
resources:
|
| 88 |
+
reservations:
|
| 89 |
+
devices:
|
| 90 |
+
- driver: nvidia
|
| 91 |
+
count: all
|
| 92 |
+
capabilities: [gpu]
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
## Windows-Specific Notes
|
| 96 |
+
|
| 97 |
+
1. **Docker Desktop Required**: Install Docker Desktop for Windows from [docker.com](https://www.docker.com/products/docker-desktop)
|
| 98 |
+
|
| 99 |
+
2. **Path Format**: Use forward slashes or PowerShell variables:
|
| 100 |
+
- PowerShell: `-v ${PWD}/output:/app/output`
|
| 101 |
+
- CMD: `-v "%CD%/output:/app/output"`
|
| 102 |
+
- Or use absolute paths: `-v C:/path/to/output:/app/output`
|
| 103 |
+
|
| 104 |
+
3. **WSL2**: Docker Desktop on Windows uses WSL2. Make sure WSL2 is enabled in Windows features.
|
| 105 |
+
|
| 106 |
+
4. **GPU Support on Windows**: Requires:
|
| 107 |
+
- WSL2 with NVIDIA GPU driver
|
| 108 |
+
- nvidia-docker installed in WSL2
|
| 109 |
+
- May not work on all Windows setups
|
| 110 |
+
|
| 111 |
+
## Troubleshooting
|
| 112 |
+
|
| 113 |
+
### Blender Not Found
|
| 114 |
+
If you get errors about Blender not being found, verify the installation:
|
| 115 |
+
```bash
|
| 116 |
+
# Linux/macOS
|
| 117 |
+
docker run --rm counterfactual-generator blender --version
|
| 118 |
+
|
| 119 |
+
# Windows PowerShell
|
| 120 |
+
docker run --rm counterfactual-generator blender --version
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
### Permission Issues
|
| 124 |
+
**Linux/macOS:**
|
| 125 |
+
```bash
|
| 126 |
+
sudo chown -R $USER:$USER output/
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
**Windows:** Usually not an issue, but if you encounter permission errors, run Docker Desktop as Administrator or adjust folder permissions.
|
| 130 |
+
|
| 131 |
+
### Interactive Shell
|
| 132 |
+
**Linux/macOS:**
|
| 133 |
+
```bash
|
| 134 |
+
docker run --rm -it -v $(pwd)/output:/app/output counterfactual-generator /bin/bash
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
**Windows (PowerShell):**
|
| 138 |
+
```powershell
|
| 139 |
+
docker run --rm -it -v ${PWD}/output:/app/output counterfactual-generator /bin/bash
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
## Notes
|
| 143 |
+
|
| 144 |
+
- The image includes Blender 4.3, which is compatible with the codebase
|
| 145 |
+
- Credentials and tokens are excluded from the Docker image for security
|
| 146 |
+
- Output files are stored in the mounted volume to persist data
|
| 147 |
+
|
Dockerfile
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use Python 3.9 slim image for smaller size
|
| 2 |
+
FROM python:3.9-slim
|
| 3 |
+
|
| 4 |
+
# 1. Install system dependencies including Xvfb for headless rendering
|
| 5 |
+
# We install these as root before switching users
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
wget \
|
| 8 |
+
xz-utils \
|
| 9 |
+
xvfb \
|
| 10 |
+
libx11-6 \
|
| 11 |
+
libxxf86vm1 \
|
| 12 |
+
libgl1 \
|
| 13 |
+
libglu1-mesa \
|
| 14 |
+
libxi6 \
|
| 15 |
+
libxrender1 \
|
| 16 |
+
libxfixes3 \
|
| 17 |
+
libfontconfig1 \
|
| 18 |
+
libxinerama1 \
|
| 19 |
+
libxkbcommon0 \
|
| 20 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 21 |
+
|
| 22 |
+
# 2. Set up a non-root user (Critical for Hugging Face Spaces)
|
| 23 |
+
# Spaces run as user ID 1000. If we don't do this, we get permission errors.
|
| 24 |
+
RUN useradd -m -u 1000 user
|
| 25 |
+
USER user
|
| 26 |
+
ENV HOME=/home/user \
|
| 27 |
+
PATH=/home/user/.local/bin:$PATH
|
| 28 |
+
|
| 29 |
+
# Set working directory to the new user's home
|
| 30 |
+
WORKDIR $HOME/app
|
| 31 |
+
|
| 32 |
+
# 3. Install Blender (User-level install)
|
| 33 |
+
# Downloading to a folder the user owns
|
| 34 |
+
RUN wget -q https://download.blender.org/release/Blender3.6/blender-3.6.8-linux-x64.tar.xz -O /tmp/blender.tar.xz && \
|
| 35 |
+
tar -xf /tmp/blender.tar.xz -C /tmp && \
|
| 36 |
+
mkdir -p $HOME/blender && \
|
| 37 |
+
mv /tmp/blender-3.6.8-linux-x64/* $HOME/blender/ && \
|
| 38 |
+
rm -rf /tmp/blender.tar.xz /tmp/blender-3.6.8-linux-x64 && \
|
| 39 |
+
# Add Blender to PATH so we can just type 'blender'
|
| 40 |
+
echo 'export PATH="$HOME/blender:$PATH"' >> $HOME/.bashrc
|
| 41 |
+
|
| 42 |
+
# Update PATH for this session as well
|
| 43 |
+
ENV PATH="$HOME/blender:$PATH"
|
| 44 |
+
|
| 45 |
+
# Verify Blender installation
|
| 46 |
+
RUN blender --version
|
| 47 |
+
|
| 48 |
+
# 4. Install Python dependencies
|
| 49 |
+
COPY --chown=user requirements.txt .
|
| 50 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 51 |
+
|
| 52 |
+
# 5. Copy application code with correct ownership
|
| 53 |
+
COPY --chown=user . .
|
| 54 |
+
|
| 55 |
+
# Create output directories that the user can write to
|
| 56 |
+
RUN mkdir -p $HOME/app/output $HOME/app/temp_output
|
| 57 |
+
|
| 58 |
+
# 6. Configuration for Hugging Face Spaces
|
| 59 |
+
# Spaces expect the app to listen on port 7860
|
| 60 |
+
ENV PORT=7860
|
| 61 |
+
EXPOSE 7860
|
| 62 |
+
|
| 63 |
+
# Healthcheck to ensure the container doesn't get killed
|
| 64 |
+
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
| 65 |
+
CMD wget --no-verbose --tries=1 --spider http://localhost:7860/_stcore/health || exit 1
|
| 66 |
+
|
| 67 |
+
# Set up Xvfb for headless rendering
|
| 68 |
+
ENV DISPLAY=:99
|
| 69 |
+
|
| 70 |
+
# 7. Run Streamlit on port 7860
|
| 71 |
+
CMD ["sh", "-c", "xvfb-run -a -s '-screen 0 1024x768x24' streamlit run app.py --server.port=7860 --server.address=0.0.0.0 --server.headless=true"]
|
README.md
CHANGED
|
@@ -1,12 +1,418 @@
|
|
| 1 |
-
---
|
| 2 |
-
title:
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: MMB Counterfactual Image Tool
|
| 3 |
+
colorFrom: blue
|
| 4 |
+
colorTo: indigo
|
| 5 |
+
sdk: docker
|
| 6 |
+
pinned: false
|
| 7 |
+
license: mit
|
| 8 |
+
arxiv: 2401.xxxxx
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
A Python-based pipeline for CLEVR-style scenes with counterfactual variants and Blender rendering. Configurable object counts, counterfactual types, and automated questions and answers.
|
| 12 |
+
|
| 13 |
+
**Key Features:**
|
| 14 |
+
- 17 different counterfactual types (9 image CFs + 8 negative CFs)
|
| 15 |
+
- Automated Blender rendering with GPU support
|
| 16 |
+
- Question-answer datasets
|
| 17 |
+
- Resume support for interrupted jobs
|
| 18 |
+
- Streamlit web interface
|
| 19 |
+
- Examples for all counterfactual types
|
| 20 |
+
|
| 21 |
+
## Hugging Face Spaces Deployment
|
| 22 |
+
|
| 23 |
+
This app is configured to run on Hugging Face Spaces using Docker. The Dockerfile includes Blender installation and all necessary dependencies for scenes and rendering.
|
| 24 |
+
|
| 25 |
+
## Features
|
| 26 |
+
|
| 27 |
+
- **Scenes**: Multiple scene sets with configurable object counts and properties
|
| 28 |
+
- **Counterfactual Variants**: Create multiple counterfactual variations per scene (color, shape, material, size changes)
|
| 29 |
+
- **Blender Rendering**: Automated rendering of scenes to PNG images using Blender's Cycles engine
|
| 30 |
+
- **Questions**: Counterfactual questions and answers for each scene set
|
| 31 |
+
- **Resume Support**: Resume interrupted rendering jobs
|
| 32 |
+
|
| 33 |
+
## Requirements
|
| 34 |
+
|
| 35 |
+
### Python Dependencies
|
| 36 |
+
|
| 37 |
+
Install Python dependencies using:
|
| 38 |
+
|
| 39 |
+
```bash
|
| 40 |
+
pip install -r requirements.txt
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
### Blender
|
| 44 |
+
|
| 45 |
+
Blender must be installed separately as it's not available via pip. The scripts are designed to run inside Blender's Python environment using:
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
blender --background --python <script.py> -- [arguments]
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
The pipeline will attempt to auto-detect Blender on your system, or you can specify the path manually.
|
| 52 |
+
|
| 53 |
+
## Installation
|
| 54 |
+
|
| 55 |
+
1. Clone this repository:
|
| 56 |
+
```bash
|
| 57 |
+
git clone <repository-url>
|
| 58 |
+
cd code
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
2. Install Python dependencies:
|
| 62 |
+
```bash
|
| 63 |
+
pip install -r requirements.txt
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
3. Ensure Blender is installed and accessible in your PATH, or note the path for manual specification.
|
| 67 |
+
|
| 68 |
+
## Usage
|
| 69 |
+
|
| 70 |
+
### Quick Start
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
# Install dependencies
|
| 74 |
+
pip install -r requirements.txt
|
| 75 |
+
|
| 76 |
+
# Generate 5 scene sets with 5 objects each
|
| 77 |
+
python pipeline.py --num_scenes 5 --num_objects 5 --run_name my_first_run
|
| 78 |
+
|
| 79 |
+
# Generate examples of all counterfactual types
|
| 80 |
+
python scripts/generate_examples.py --render
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### Web Interface (Streamlit)
|
| 84 |
+
|
| 85 |
+
Run the Streamlit web interface for an interactive experience:
|
| 86 |
+
|
| 87 |
+
```bash
|
| 88 |
+
streamlit run app.py
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
The web interface provides:
|
| 92 |
+
- Interactive parameter configuration
|
| 93 |
+
- Real-time progress
|
| 94 |
+
- Visual output preview
|
| 95 |
+
- Downloadable results as ZIP files
|
| 96 |
+
|
| 97 |
+
### 1. Generate and Render (Combined)
|
| 98 |
+
|
| 99 |
+
Use `python pipeline.py` to generate scene JSON files and render PNG images in one step:
|
| 100 |
+
|
| 101 |
+
```bash
|
| 102 |
+
python pipeline.py --num_scenes 10 --num_objects 5 --run_name experiment1
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
### 2. Generate Only (Skip Rendering)
|
| 106 |
+
|
| 107 |
+
Generate scenes without rendering:
|
| 108 |
+
|
| 109 |
+
```bash
|
| 110 |
+
python pipeline.py --num_scenes 10 --num_objects 5 --run_name experiment1 --skip_render
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
### 4. Render Existing Scenes Only
|
| 114 |
+
|
| 115 |
+
Render previously generated scene JSON files:
|
| 116 |
+
|
| 117 |
+
```bash
|
| 118 |
+
# Render scenes from a specific run
|
| 119 |
+
python pipeline.py --render_only --run_name experiment1
|
| 120 |
+
|
| 121 |
+
# Auto-detect latest run
|
| 122 |
+
python pipeline.py --render_only --auto_latest
|
| 123 |
+
|
| 124 |
+
# Use GPU rendering
|
| 125 |
+
python pipeline.py --render_only --run_name experiment1 --use_gpu 1
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
This allows you to:
|
| 129 |
+
- Generate scenes on one machine, render on another
|
| 130 |
+
- Generate many scenes, then render them in batches
|
| 131 |
+
- Re-render scenes with different settings (GPU/CPU, samples, resolution) without regenerating
|
| 132 |
+
|
| 133 |
+
### 5. Generating Scenes and Images (Full Options)
|
| 134 |
+
|
| 135 |
+
#### Arguments
|
| 136 |
+
|
| 137 |
+
| Argument | Default | Description |
|
| 138 |
+
| ------------------------- | ------------- | ---------------------------------------------------------------------------- |
|
| 139 |
+
| `--num_scenes` | 5 | Number of scene sets to generate |
|
| 140 |
+
| `--num_objects` | None | Fixed number of objects per scene (overrides min/max) |
|
| 141 |
+
| `--min_objects` | 3 | Minimum object count (if `num_objects` not given) |
|
| 142 |
+
| `--max_objects` | 7 | Maximum object count (if `num_objects` not given) |
|
| 143 |
+
| `--num_counterfactuals` | 2 | Number of counterfactual variants per scene |
|
| 144 |
+
| `--blender_path` | auto-detected | Path to Blender executable |
|
| 145 |
+
| `--output_dir` | `output` | Base directory for all runs (each run gets a timestamped/named subfolder) |
|
| 146 |
+
| `--run_name` | None | Optional custom name for this run (creates `output/run_name/`) |
|
| 147 |
+
| `--use_gpu` | 0 | 1 = enable GPU rendering, 0 = CPU rendering |
|
| 148 |
+
| `--samples` | 512 | Cycles sampling rate (higher = better quality, slower) |
|
| 149 |
+
| `--width` | 320 | Image width in pixels |
|
| 150 |
+
| `--height` | 240 | Image height in pixels |
|
| 151 |
+
| `--skip_render` | False | Generate scenes only (JSON only, no PNG rendering) |
|
| 152 |
+
| `--render_only` | False | Only render existing scene JSON files. Requires --run_dir, --run_name, or --auto_latest |
|
| 153 |
+
| `--run_dir` | None | Run directory containing scenes/ folder (for --render_only mode) |
|
| 154 |
+
| `--auto_latest` | False | Automatically use the latest run in output_dir (for --render_only mode) |
|
| 155 |
+
| `--resume` | False | Resume from where it stopped in an existing run |
|
| 156 |
+
| `--cf_types` | None | Explicitly choose which counterfactual types are used (order is respected; cycles if needed) |
|
| 157 |
+
| `--same_cf_type` | False | Use the same counterfactual type for all variants (first in `--cf_types`, or one random if unset) |
|
| 158 |
+
| `--list_cf_types` | False | Print available counterfactual types and exit |
|
| 159 |
+
| `--min_cf_change_score` | 1.0 | Minimum heuristic change score (retries CF until it’s “noticeable” enough) |
|
| 160 |
+
| `--max_cf_attempts` | 10 | Max retries per counterfactual to reach `--min_cf_change_score` |
|
| 161 |
+
|
| 162 |
+
#### Examples
|
| 163 |
+
|
| 164 |
+
```bash
|
| 165 |
+
# Generate 10 scene sets with 5 objects each
|
| 166 |
+
python3 pipeline.py --num_scenes 10 --num_objects 5 --run_name experiment1
|
| 167 |
+
|
| 168 |
+
# Generate 10 scene sets with 3-7 objects, using GPU
|
| 169 |
+
python3 pipeline.py --num_scenes 10 --min_objects 3 --max_objects 7 --use_gpu 1
|
| 170 |
+
|
| 171 |
+
# Resume from where it stopped
|
| 172 |
+
python3 pipeline.py --num_scenes 100 --run_name experiment1 --resume
|
| 173 |
+
|
| 174 |
+
# Generate scenes only (no rendering)
|
| 175 |
+
python3 pipeline.py --num_scenes 10 --skip_render
|
| 176 |
+
|
| 177 |
+
# List available counterfactual types
|
| 178 |
+
python3 pipeline.py --list_cf_types
|
| 179 |
+
|
| 180 |
+
# Force specific counterfactual types in order (CF1 then CF2)
|
| 181 |
+
python3 pipeline.py --num_scenes 10 --num_counterfactuals 2 --cf_types change_color add_noise
|
| 182 |
+
# All counterfactuals same type (e.g. both change_color):
|
| 183 |
+
python3 pipeline.py --num_scenes 10 --num_counterfactuals 2 --cf_types change_color --same_cf_type
|
| 184 |
+
|
| 185 |
+
# Make counterfactuals more noticeable (stricter threshold + more retries)
|
| 186 |
+
python3 pipeline.py --num_scenes 10 --min_cf_change_score 1.5 --max_cf_attempts 25
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
#### Counterfactual Types
|
| 190 |
+
|
| 191 |
+
The pipeline supports 17 different counterfactual types, divided into two categories:
|
| 192 |
+
|
| 193 |
+
**Image Counterfactuals** (Should change VQA answers - 10 types):
|
| 194 |
+
- `change_color` - Change the color of a random object (e.g., red → blue)
|
| 195 |
+
- `change_shape` - Change the shape of a random object (cube/sphere/cylinder)
|
| 196 |
+
- `change_size` - Change the size of a random object (small ↔ large)
|
| 197 |
+
- `change_material` - Change the material of a random object (metal ↔ rubber)
|
| 198 |
+
- `change_position` - Move a random object to a different location (with collision detection)
|
| 199 |
+
- `add_object` - Add a new random object to the scene
|
| 200 |
+
- `remove_object` - Remove a random object from the scene
|
| 201 |
+
- `replace_object` - Replace an object with a different one (keeping position)
|
| 202 |
+
- `swap_attribute` - Swap an attribute (e.g. color) between two objects
|
| 203 |
+
- `relational_flip` - Move object from left of X to right of X
|
| 204 |
+
|
| 205 |
+
**Negative Counterfactuals** (Should NOT change VQA answers - 8 types):
|
| 206 |
+
- `change_background` - Change the background/ground color
|
| 207 |
+
- `change_lighting` - Change lighting conditions (bright/dim/warm/cool/dramatic)
|
| 208 |
+
- `add_noise` - Add image noise/grain (light/medium/heavy levels)
|
| 209 |
+
- `occlusion_change` - Move object to partially hide another (visual only; answers unchanged)
|
| 210 |
+
- `apply_fisheye` - Apply fisheye lens distortion effect
|
| 211 |
+
- `apply_blur` - Apply Gaussian blur filter
|
| 212 |
+
- `apply_vignette` - Apply vignette effect (edge darkening)
|
| 213 |
+
- `apply_chromatic_aberration` - Apply chromatic aberration (color fringing)
|
| 214 |
+
|
| 215 |
+
**Default Behavior**: If `--cf_types` is not specified, the pipeline uses a default mix of 1 image counterfactual + 1 negative counterfactual per scene.
|
| 216 |
+
|
| 217 |
+
**Usage Examples**:
|
| 218 |
+
```bash
|
| 219 |
+
# List all available counterfactual types
|
| 220 |
+
python3 pipeline.py --list_cf_types
|
| 221 |
+
|
| 222 |
+
# Use specific counterfactual types (order is respected; cycles if needed)
|
| 223 |
+
python3 pipeline.py --num_scenes 10 --num_counterfactuals 2 --cf_types change_color change_position
|
| 224 |
+
|
| 225 |
+
# Mix image and negative counterfactuals
|
| 226 |
+
python3 pipeline.py --num_scenes 10 --cf_types change_shape change_lighting add_noise
|
| 227 |
+
|
| 228 |
+
# Use only negative counterfactuals
|
| 229 |
+
python3 pipeline.py --num_scenes 10 --cf_types change_background add_noise apply_blur
|
| 230 |
+
|
| 231 |
+
# Use only image counterfactuals
|
| 232 |
+
python3 pipeline.py --num_scenes 10 --cf_types change_color change_shape change_position
|
| 233 |
+
```
|
| 234 |
+
|
| 235 |
+
### 2. Generating Example Counterfactuals
|
| 236 |
+
|
| 237 |
+
Generate examples of all counterfactual types applied to a base scene:
|
| 238 |
+
|
| 239 |
+
```bash
|
| 240 |
+
# Generate scene JSON files only
|
| 241 |
+
python scripts/generate_examples.py
|
| 242 |
+
|
| 243 |
+
# Generate and render to images
|
| 244 |
+
python scripts/generate_examples.py --render
|
| 245 |
+
|
| 246 |
+
# Customize output directory and number of objects
|
| 247 |
+
python scripts/generate_examples.py --render --output_dir output/my_examples --num_objects 7
|
| 248 |
+
|
| 249 |
+
# Use GPU rendering
|
| 250 |
+
python scripts/generate_examples.py --render --use_gpu 1
|
| 251 |
+
```
|
| 252 |
+
|
| 253 |
+
This creates one example of each counterfactual type in `output/counterfactual_examples/`:
|
| 254 |
+
- Original scene
|
| 255 |
+
- 9 image counterfactual examples
|
| 256 |
+
- 8 negative counterfactual examples
|
| 257 |
+
|
| 258 |
+
### 3. Generating Question Mappings
|
| 259 |
+
|
| 260 |
+
Use `scripts/generate_questions_mapping.py` to create a CSV with counterfactual questions and answers.
|
| 261 |
+
|
| 262 |
+
#### Arguments
|
| 263 |
+
|
| 264 |
+
| Argument | Default | Description |
|
| 265 |
+
| ---------------------- | ---------------------------------- | -------------------------------------------------------------------------------- |
|
| 266 |
+
| `--output_dir` | `output` | Run directory or base output directory |
|
| 267 |
+
| `--auto_latest` | False | Automatically find and use the latest run in `output_dir` |
|
| 268 |
+
| `--csv_name` | `image_mapping_with_questions.csv` | Name of the generated CSV file |
|
| 269 |
+
| `--generate_questions` | False | Generate questions and answers (without this flag, only outputs image filenames) |
|
| 270 |
+
| `--long_format` | False | Also output a long-format QA dataset CSV (one row per image-question pair) |
|
| 271 |
+
| `--long_csv_name` | `qa_dataset.csv` | Filename for the long-format QA dataset CSV |
|
| 272 |
+
|
| 273 |
+
#### Examples
|
| 274 |
+
|
| 275 |
+
```bash
|
| 276 |
+
# Generate questions for a specific run
|
| 277 |
+
python scripts/generate_questions_mapping.py --output_dir output/experiment1 --generate_questions
|
| 278 |
+
|
| 279 |
+
# Auto-detect latest run and generate questions
|
| 280 |
+
python scripts/generate_questions_mapping.py --output_dir output --auto_latest --generate_questions
|
| 281 |
+
|
| 282 |
+
# Also generate a long-format QA dataset (includes correct answer labels/ids)
|
| 283 |
+
python scripts/generate_questions_mapping.py --output_dir output --auto_latest --generate_questions --long_format --long_csv_name qa_dataset.csv
|
| 284 |
+
```
|
| 285 |
+
|
| 286 |
+
#### Ensuring counterfactual answers differ
|
| 287 |
+
|
| 288 |
+
For evaluation and datasets, the **counterfactual image’s answer to its counterfactual question** should differ from the **original image’s answer to the original question**. The pipeline does this in two ways:
|
| 289 |
+
|
| 290 |
+
1. **Automatic retries**
|
| 291 |
+
When generating questions (with `--generate_questions`), the script retries up to **`MAX_CF_ANSWER_RETRIES`** (default 50) per scene. For each attempt it picks new counterfactual questions (with different randomness). It keeps a pair only when both CF1 and CF2 answers differ from the original answer (after normalizing, e.g. case and whitespace). If after all retries they still match, the scene is still included and a warning is printed.
|
| 292 |
+
|
| 293 |
+
2. **CF-specific question templates**
|
| 294 |
+
Counterfactual questions are chosen from templates that target the **changed** attribute or count, so the answer on the counterfactual image is different by design:
|
| 295 |
+
- **Count-changing CFs** (e.g. `add_object`, `remove_object`): questions like “How many objects are in the scene?” or “Are there more than N objects?” so the count/yes-no differs.
|
| 296 |
+
- **Attribute-changing CFs** (e.g. `change_color`, `change_shape`): questions about the **new** value (e.g. “How many red objects?” when an object was changed to red), so the count on the CF image differs from the original.
|
| 297 |
+
|
| 298 |
+
**What you need to do:**
|
| 299 |
+
|
| 300 |
+
- Run question generation **after** scenes and images exist (either as part of the pipeline with `--generate_questions`, or later on a run directory):
|
| 301 |
+
|
| 302 |
+
```bash
|
| 303 |
+
# As part of a full run (ensures answers differ for that run’s scenes)
|
| 304 |
+
python pipeline.py --num_scenes 10 --num_objects 5 --run_name my_run --generate_questions
|
| 305 |
+
|
| 306 |
+
# Or later, on an existing run
|
| 307 |
+
python scripts/generate_questions_mapping.py --output_dir output/my_run --generate_questions
|
| 308 |
+
```
|
| 309 |
+
|
| 310 |
+
- **Optional:** To allow more attempts per scene, edit `scripts/generate_questions_mapping.py` and increase **`MAX_CF_ANSWER_RETRIES`** (e.g. from 50 to 100). No CLI flag is exposed for this.
|
| 311 |
+
|
| 312 |
+
**720p dataset with different answers (distinct CF types per scene):**
|
| 313 |
+
|
| 314 |
+
- Scenes use **two distinct counterfactual types** per scene (no duplicate CF type for CF1 and CF2). To generate a 720p (1280×720) set with question/answer validation and optional removal of scenes where the CF answer matches the original:
|
| 315 |
+
|
| 316 |
+
```bash
|
| 317 |
+
python pipeline.py --num_scenes 1000 --output_dir output --run_name dataset_720p_diff --width 1280 --height 720 --semantic_only --num_counterfactuals 2 --generate_questions --filter_same_answer
|
| 318 |
+
```
|
| 319 |
+
|
| 320 |
+
- This produces `output/dataset_720p_diff/` with scene sets where CF1 and CF2 use different types, questions are validated (original + CFs), and rows where the CF image’s answer to the CF question matches the original are removed. Increase `MAX_CF_ANSWER_RETRIES` in `scripts/generate_questions_mapping.py` if you want fewer “could not find valid questions” warnings.
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
## Project Structure
|
| 324 |
+
|
| 325 |
+
```
|
| 326 |
+
.
|
| 327 |
+
├── pipeline.py # Main pipeline and rendering (supports --render_only)
|
| 328 |
+
├── app.py # Streamlit web interface
|
| 329 |
+
├── requirements.txt # Python dependencies
|
| 330 |
+
├── data/ # Scene assets
|
| 331 |
+
│ ├── base_scene.blend # Base Blender scene
|
| 332 |
+
│ ├── CoGenT_A.json # Scene configuration A
|
| 333 |
+
│ ├── CoGenT_B.json # Scene configuration B
|
| 334 |
+
│ ├── properties.json # Object properties configuration
|
| 335 |
+
│ ├── materials/ # Material definitions
|
| 336 |
+
│ └── shapes/ # 3D shape definitions
|
| 337 |
+
├── scripts/ # Scripts directory
|
| 338 |
+
│ ├── render.py # Blender rendering script (run by pipeline)
|
| 339 |
+
│ ├── generate_scenes.py # Generate scene JSON files only (alternative to --skip_render)
|
| 340 |
+
│ ├── generate_examples.py # Generate example counterfactuals
|
| 341 |
+
│ └── generate_questions_mapping.py # Generate question-answer datasets
|
| 342 |
+
├── output/ # Output directory (generated)
|
| 343 |
+
│ └── counterfactual_examples/ # Example counterfactuals (from generate_examples.py)
|
| 344 |
+
├── .gitignore # Git ignore rules
|
| 345 |
+
└── README.md # This file
|
| 346 |
+
|
| 347 |
+
**Note**: The pipeline dynamically generates temporary files during execution:
|
| 348 |
+
- `render_images_patched.py` - Patched rendering script
|
| 349 |
+
- `temp_output/<run_id>/` - Temporary output per run (e.g. `temp_output/2025-01-29_12-30-45/images` and `.../scenes`), so different runs use separate directories and do not overwrite each other.
|
| 350 |
+
|
| 351 |
+
These files are automatically created when needed, cleaned up after use, and are ignored by git.
|
| 352 |
+
```
|
| 353 |
+
|
| 354 |
+
## Output Structure
|
| 355 |
+
|
| 356 |
+
Each run creates a directory structure like:
|
| 357 |
+
|
| 358 |
+
```
|
| 359 |
+
output/
|
| 360 |
+
└── <run_name_or_timestamp>/
|
| 361 |
+
├── checkpoint.json
|
| 362 |
+
├── run_metadata.json
|
| 363 |
+
├── scenes/
|
| 364 |
+
│ ├── scene_0000_original.json
|
| 365 |
+
│ ├── scene_0000_cf1.json
|
| 366 |
+
│ ├── scene_0000_cf2.json
|
| 367 |
+
│ └── ...
|
| 368 |
+
├── images/
|
| 369 |
+
│ ├── scene_0000_original.png
|
| 370 |
+
│ ├── scene_0000_cf1.png
|
| 371 |
+
│ ├── scene_0000_cf2.png
|
| 372 |
+
│ └── ...
|
| 373 |
+
├── image_mapping_with_questions*.csv
|
| 374 |
+
└── qa_dataset*.csv (if generated with --long_format)
|
| 375 |
+
```
|
| 376 |
+
|
| 377 |
+
### Dataset columns (high level)
|
| 378 |
+
|
| 379 |
+
**Wide Format CSV** (default with `--generate_questions`):
|
| 380 |
+
- **Image columns**: `original_image`, `counterfactual1_image`, `counterfactual2_image`
|
| 381 |
+
- **Question columns**: `original_question`, `counterfactual1_question`, `counterfactual2_question`
|
| 382 |
+
- **Answer matrix columns** (9 total, showing each image's answer to each question):
|
| 383 |
+
- `original_image_answer_to_original_question`
|
| 384 |
+
- `original_image_answer_to_cf1_question`
|
| 385 |
+
- `original_image_answer_to_cf2_question`
|
| 386 |
+
- `cf1_image_answer_to_original_question`
|
| 387 |
+
- `cf1_image_answer_to_cf1_question`
|
| 388 |
+
- `cf1_image_answer_to_cf2_question`
|
| 389 |
+
- `cf2_image_answer_to_original_question`
|
| 390 |
+
- `cf2_image_answer_to_cf1_question`
|
| 391 |
+
- `cf2_image_answer_to_cf2_question`
|
| 392 |
+
|
| 393 |
+
**Basic Format CSV** (without `--generate_questions`):
|
| 394 |
+
- Only image columns: `original_image`, `counterfactual1_image`, `counterfactual2_image`
|
| 395 |
+
|
| 396 |
+
## Code Style
|
| 397 |
+
|
| 398 |
+
The codebase follows a clean, consistent style:
|
| 399 |
+
- No multiline docstrings (removed for brevity)
|
| 400 |
+
- Consistent spacing and formatting
|
| 401 |
+
- Single blank lines between functions
|
| 402 |
+
- Clear, concise function names
|
| 403 |
+
|
| 404 |
+
## License
|
| 405 |
+
|
| 406 |
+
MIT License
|
| 407 |
+
|
| 408 |
+
## Contributing
|
| 409 |
+
|
| 410 |
+
Contributions are welcome! Please ensure:
|
| 411 |
+
- Code follows the existing style (no multiline docstrings, consistent spacing)
|
| 412 |
+
- All tests pass
|
| 413 |
+
- Documentation is updated as needed
|
| 414 |
+
|
| 415 |
+
## Acknowledgments
|
| 416 |
+
|
| 417 |
+
This project is inspired by the CLEVR dataset and uses Blender for 3D scene rendering.
|
| 418 |
+
|
analyze_semantic_failures.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import csv
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def main() -> None:
|
| 6 |
+
base_dir = Path(__file__).resolve().parent
|
| 7 |
+
|
| 8 |
+
rel_path = base_dir / "output" / "200samples_mmib_rel" / "image_mapping_with_questions_200_rel.csv"
|
| 9 |
+
sem_path = base_dir / "output" / "200samples_mmib_rel" / "image_mapping_with_questions_200_semantic.csv"
|
| 10 |
+
|
| 11 |
+
with rel_path.open(newline="", encoding="utf-8") as f:
|
| 12 |
+
rel_reader = csv.DictReader(f)
|
| 13 |
+
rel_rows = list(rel_reader)
|
| 14 |
+
|
| 15 |
+
with sem_path.open(newline="", encoding="utf-8") as f:
|
| 16 |
+
sem_reader = csv.DictReader(f)
|
| 17 |
+
sem_rows = list(sem_reader)
|
| 18 |
+
|
| 19 |
+
# Keys identifying a CF pair
|
| 20 |
+
def key_from_rel(row: dict) -> tuple[str, str]:
|
| 21 |
+
return row["original_image"], row["counterfactual_image"]
|
| 22 |
+
|
| 23 |
+
def key_from_sem(row: dict) -> tuple[str, str]:
|
| 24 |
+
return row["original_image"], row["counterfactual_image"]
|
| 25 |
+
|
| 26 |
+
sem_keys = {key_from_sem(r) for r in sem_rows}
|
| 27 |
+
|
| 28 |
+
missing = [r for r in rel_rows if key_from_rel(r) not in sem_keys]
|
| 29 |
+
|
| 30 |
+
print(f"Total rel rows: {len(rel_rows)}")
|
| 31 |
+
print(f"Total semantic rows: {len(sem_rows)}")
|
| 32 |
+
print(f"Missing (failed) pairs: {len(missing)}\n")
|
| 33 |
+
|
| 34 |
+
for r in missing:
|
| 35 |
+
print(
|
| 36 |
+
f"{r['original_image']} -> {r['counterfactual_image']} | "
|
| 37 |
+
f"type={r.get('counterfactual_type','')} | desc={r.get('counterfactual_description','')}"
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
if __name__ == "__main__":
|
| 42 |
+
main()
|
| 43 |
+
|
app.py
ADDED
|
@@ -0,0 +1,1058 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
import tempfile
|
| 5 |
+
import zipfile
|
| 6 |
+
import json
|
| 7 |
+
import random
|
| 8 |
+
import math
|
| 9 |
+
import csv
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
import time
|
| 13 |
+
|
| 14 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 15 |
+
|
| 16 |
+
script_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'scripts')
|
| 17 |
+
sys.path.insert(0, script_dir)
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
from pipeline import (
|
| 21 |
+
generate_counterfactuals,
|
| 22 |
+
generate_base_scene,
|
| 23 |
+
save_scene,
|
| 24 |
+
render_scene,
|
| 25 |
+
create_patched_render_script,
|
| 26 |
+
IMAGE_COUNTERFACTUALS,
|
| 27 |
+
NEGATIVE_COUNTERFACTUALS
|
| 28 |
+
)
|
| 29 |
+
try:
|
| 30 |
+
import sys
|
| 31 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 32 |
+
scripts_path = os.path.join(script_dir, 'scripts')
|
| 33 |
+
if scripts_path not in sys.path:
|
| 34 |
+
sys.path.insert(0, scripts_path)
|
| 35 |
+
from generate_questions_mapping import (
|
| 36 |
+
load_scene,
|
| 37 |
+
generate_question_for_scene as _generate_question_for_scene_file,
|
| 38 |
+
answer_question_for_scene,
|
| 39 |
+
generate_mapping_with_questions
|
| 40 |
+
)
|
| 41 |
+
except ImportError:
|
| 42 |
+
def load_scene(scene_file):
|
| 43 |
+
with open(scene_file, 'r') as f:
|
| 44 |
+
return json.load(f)
|
| 45 |
+
def answer_question_for_scene(question, scene):
|
| 46 |
+
objects = scene.get('objects', [])
|
| 47 |
+
return len(objects)
|
| 48 |
+
_generate_question_for_scene_file = None
|
| 49 |
+
generate_mapping_with_questions = None
|
| 50 |
+
PIPELINE_AVAILABLE = True
|
| 51 |
+
except ImportError as e:
|
| 52 |
+
print(f"Warning: Error importing pipeline functions: {e}")
|
| 53 |
+
PIPELINE_AVAILABLE = False
|
| 54 |
+
answer_question_for_scene = None
|
| 55 |
+
load_scene = None
|
| 56 |
+
_generate_question_for_scene_file = None
|
| 57 |
+
|
| 58 |
+
st.set_page_config(
|
| 59 |
+
page_title="Counterfactual Image Generator",
|
| 60 |
+
page_icon="🎨",
|
| 61 |
+
layout="wide",
|
| 62 |
+
initial_sidebar_state="expanded"
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
st.markdown("""
|
| 66 |
+
<style>
|
| 67 |
+
.main-header {
|
| 68 |
+
font-size: 2.5rem;
|
| 69 |
+
font-weight: bold;
|
| 70 |
+
color: #1f77b4;
|
| 71 |
+
text-align: center;
|
| 72 |
+
margin-bottom: 2rem;
|
| 73 |
+
}
|
| 74 |
+
.stButton>button {
|
| 75 |
+
width: 100%;
|
| 76 |
+
height: 3.5rem;
|
| 77 |
+
font-size: 1.2rem;
|
| 78 |
+
font-weight: bold;
|
| 79 |
+
background-color: #1f77b4;
|
| 80 |
+
color: white;
|
| 81 |
+
border-radius: 0.5rem;
|
| 82 |
+
}
|
| 83 |
+
.stButton>button:hover {
|
| 84 |
+
background-color: #1565c0;
|
| 85 |
+
}
|
| 86 |
+
.info-box {
|
| 87 |
+
padding: 1rem;
|
| 88 |
+
border-radius: 0.5rem;
|
| 89 |
+
background-color: #f0f2f6;
|
| 90 |
+
margin: 1rem 0;
|
| 91 |
+
}
|
| 92 |
+
</style>
|
| 93 |
+
""", unsafe_allow_html=True)
|
| 94 |
+
|
| 95 |
+
def create_zip_file(output_dir, zip_path):
|
| 96 |
+
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
| 97 |
+
for root, dirs, files in os.walk(output_dir):
|
| 98 |
+
for file in files:
|
| 99 |
+
file_path = os.path.join(root, file)
|
| 100 |
+
arcname = os.path.relpath(file_path, output_dir)
|
| 101 |
+
zipf.write(file_path, arcname)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def generate_fallback_scene(num_objects, scene_idx):
|
| 105 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 106 |
+
props_path = os.path.join(script_dir, 'data', 'properties.json')
|
| 107 |
+
|
| 108 |
+
try:
|
| 109 |
+
with open(props_path, 'r') as f:
|
| 110 |
+
properties = json.load(f)
|
| 111 |
+
except:
|
| 112 |
+
properties = {
|
| 113 |
+
'shapes': {'cube': 'SmoothCube_v2', 'sphere': 'Sphere', 'cylinder': 'SmoothCylinder'},
|
| 114 |
+
'colors': {'gray': [87, 87, 87], 'red': [173, 35, 35], 'blue': [42, 75, 215],
|
| 115 |
+
'green': [29, 105, 20], 'brown': [129, 74, 25], 'purple': [129, 38, 192],
|
| 116 |
+
'cyan': [41, 208, 208], 'yellow': [255, 238, 51]},
|
| 117 |
+
'materials': {'rubber': 'Rubber', 'metal': 'MyMetal'},
|
| 118 |
+
'sizes': {'large': 0.7, 'small': 0.35}
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
shapes = list(properties['shapes'].keys())
|
| 122 |
+
colors = list(properties['colors'].keys())
|
| 123 |
+
materials = list(properties['materials'].keys())
|
| 124 |
+
sizes = list(properties['sizes'].keys())
|
| 125 |
+
|
| 126 |
+
scene_num = scene_idx + 1
|
| 127 |
+
scene = {
|
| 128 |
+
'split': 'fallback',
|
| 129 |
+
'image_index': scene_num,
|
| 130 |
+
'image_filename': f'scene_{scene_num:04d}_original.png',
|
| 131 |
+
'objects': [],
|
| 132 |
+
'directions': {
|
| 133 |
+
'behind': (0.0, -1.0, 0.0),
|
| 134 |
+
'front': (0.0, 1.0, 0.0),
|
| 135 |
+
'left': (-1.0, 0.0, 0.0),
|
| 136 |
+
'right': (1.0, 0.0, 0.0),
|
| 137 |
+
'above': (0.0, 0.0, 1.0),
|
| 138 |
+
'below': (0.0, 0.0, -1.0)
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
positions = []
|
| 143 |
+
min_dist = 0.25
|
| 144 |
+
|
| 145 |
+
for i in range(num_objects):
|
| 146 |
+
max_attempts = 100
|
| 147 |
+
placed = False
|
| 148 |
+
|
| 149 |
+
for attempt in range(max_attempts):
|
| 150 |
+
x = random.uniform(-3, 3)
|
| 151 |
+
y = random.uniform(-3, 3)
|
| 152 |
+
z = random.uniform(0.35, 0.7)
|
| 153 |
+
|
| 154 |
+
collision = False
|
| 155 |
+
size = random.choice(sizes)
|
| 156 |
+
r = properties['sizes'][size]
|
| 157 |
+
|
| 158 |
+
for (px, py, pz, pr) in positions:
|
| 159 |
+
dist = math.sqrt((x - px)**2 + (y - py)**2)
|
| 160 |
+
if dist < (r + pr + min_dist):
|
| 161 |
+
collision = True
|
| 162 |
+
break
|
| 163 |
+
|
| 164 |
+
if not collision:
|
| 165 |
+
positions.append((x, y, z, r))
|
| 166 |
+
placed = True
|
| 167 |
+
break
|
| 168 |
+
|
| 169 |
+
if not placed:
|
| 170 |
+
x = random.uniform(-3, 3)
|
| 171 |
+
y = random.uniform(-3, 3)
|
| 172 |
+
z = random.uniform(0.35, 0.7)
|
| 173 |
+
size = random.choice(sizes)
|
| 174 |
+
r = properties['sizes'][size]
|
| 175 |
+
positions.append((x, y, z, r))
|
| 176 |
+
|
| 177 |
+
shape = random.choice(shapes)
|
| 178 |
+
color = random.choice(colors)
|
| 179 |
+
material = random.choice(materials)
|
| 180 |
+
|
| 181 |
+
obj = {
|
| 182 |
+
'shape': shape,
|
| 183 |
+
'size': size,
|
| 184 |
+
'material': material,
|
| 185 |
+
'3d_coords': [x, y, z],
|
| 186 |
+
'rotation': random.uniform(0, 360),
|
| 187 |
+
'pixel_coords': [0, 0, 0],
|
| 188 |
+
'color': color
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
scene['objects'].append(obj)
|
| 192 |
+
|
| 193 |
+
return scene
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def generate_question_for_scene_dict(scene):
|
| 197 |
+
if _generate_question_for_scene_file is None:
|
| 198 |
+
objects = scene.get('objects', [])
|
| 199 |
+
if len(objects) == 0:
|
| 200 |
+
return "How many objects are in the scene?", {}
|
| 201 |
+
|
| 202 |
+
colors = list(set(obj.get('color') for obj in objects if obj.get('color')))
|
| 203 |
+
shapes = list(set(obj.get('shape') for obj in objects if obj.get('shape')))
|
| 204 |
+
|
| 205 |
+
if colors:
|
| 206 |
+
return f"How many {random.choice(colors)} objects are there?", {'color': random.choice(colors)}
|
| 207 |
+
else:
|
| 208 |
+
return "How many objects are in the scene?", {}
|
| 209 |
+
|
| 210 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp_file:
|
| 211 |
+
json.dump(scene, tmp_file)
|
| 212 |
+
tmp_path = tmp_file.name
|
| 213 |
+
|
| 214 |
+
try:
|
| 215 |
+
question, params = _generate_question_for_scene_file(tmp_path)
|
| 216 |
+
return question, params
|
| 217 |
+
finally:
|
| 218 |
+
try:
|
| 219 |
+
os.unlink(tmp_path)
|
| 220 |
+
except:
|
| 221 |
+
pass
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def generate_counterfactual_scenes(num_scenes, num_objects, min_objects, max_objects, num_counterfactuals,
|
| 225 |
+
cf_types, same_cf_type, min_change_score, max_cf_attempts, min_noise_level,
|
| 226 |
+
output_dir, blender_path=None, use_gpu=0, samples=512,
|
| 227 |
+
width=320, height=240, skip_render=False, generate_questions=False):
|
| 228 |
+
if not PIPELINE_AVAILABLE:
|
| 229 |
+
return {
|
| 230 |
+
'success': False,
|
| 231 |
+
'error': 'Pipeline functions not available. Please ensure pipeline.py is accessible.'
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
scenes_dir = os.path.join(output_dir, 'scenes')
|
| 235 |
+
images_dir = os.path.join(output_dir, 'images')
|
| 236 |
+
os.makedirs(scenes_dir, exist_ok=True)
|
| 237 |
+
os.makedirs(images_dir, exist_ok=True)
|
| 238 |
+
|
| 239 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 240 |
+
cwd = os.getcwd()
|
| 241 |
+
import shutil
|
| 242 |
+
import time
|
| 243 |
+
|
| 244 |
+
temp_output_dir = os.path.join(cwd, 'temp_output')
|
| 245 |
+
if os.path.exists(temp_output_dir):
|
| 246 |
+
for attempt in range(3):
|
| 247 |
+
try:
|
| 248 |
+
shutil.rmtree(temp_output_dir)
|
| 249 |
+
break
|
| 250 |
+
except Exception as e:
|
| 251 |
+
if attempt < 2:
|
| 252 |
+
time.sleep(0.3)
|
| 253 |
+
else:
|
| 254 |
+
print(f"Warning: Could not remove temp_output after 3 attempts: {e}")
|
| 255 |
+
|
| 256 |
+
render_patched_path = os.path.join(cwd, 'render_images_patched.py')
|
| 257 |
+
if os.path.exists(render_patched_path):
|
| 258 |
+
for attempt in range(3):
|
| 259 |
+
try:
|
| 260 |
+
time.sleep(0.2)
|
| 261 |
+
if os.path.exists(render_patched_path):
|
| 262 |
+
os.remove(render_patched_path)
|
| 263 |
+
break
|
| 264 |
+
except Exception as e:
|
| 265 |
+
if attempt < 2:
|
| 266 |
+
time.sleep(0.3)
|
| 267 |
+
else:
|
| 268 |
+
print(f"Warning: Could not remove render_images_patched.py after 3 attempts: {e}")
|
| 269 |
+
|
| 270 |
+
blender_available = False
|
| 271 |
+
if blender_path is None:
|
| 272 |
+
try:
|
| 273 |
+
from pipeline import find_blender
|
| 274 |
+
blender_path = find_blender()
|
| 275 |
+
except:
|
| 276 |
+
blender_path = 'blender'
|
| 277 |
+
|
| 278 |
+
if blender_path and blender_path != 'blender':
|
| 279 |
+
blender_available = os.path.exists(blender_path)
|
| 280 |
+
else:
|
| 281 |
+
try:
|
| 282 |
+
import subprocess
|
| 283 |
+
test_path = blender_path if blender_path and blender_path != 'blender' else 'blender'
|
| 284 |
+
env = os.environ.copy()
|
| 285 |
+
result = subprocess.run([test_path, '--version'], capture_output=True, timeout=5, env=env)
|
| 286 |
+
blender_available = (result.returncode == 0)
|
| 287 |
+
except:
|
| 288 |
+
blender_available = False
|
| 289 |
+
|
| 290 |
+
successful_scenes = 0
|
| 291 |
+
successful_renders = 0
|
| 292 |
+
error_messages = []
|
| 293 |
+
|
| 294 |
+
try:
|
| 295 |
+
for scene_idx in range(num_scenes):
|
| 296 |
+
if num_objects is not None:
|
| 297 |
+
scene_num_objects = num_objects
|
| 298 |
+
else:
|
| 299 |
+
scene_num_objects = random.randint(min_objects, max_objects)
|
| 300 |
+
|
| 301 |
+
base_scene = None
|
| 302 |
+
|
| 303 |
+
if blender_available:
|
| 304 |
+
scene_error = None
|
| 305 |
+
for retry in range(3):
|
| 306 |
+
try:
|
| 307 |
+
import io
|
| 308 |
+
import contextlib
|
| 309 |
+
output_buffer = io.StringIO()
|
| 310 |
+
with contextlib.redirect_stdout(output_buffer), contextlib.redirect_stderr(output_buffer):
|
| 311 |
+
base_scene = generate_base_scene(
|
| 312 |
+
scene_num_objects,
|
| 313 |
+
blender_path,
|
| 314 |
+
scene_idx
|
| 315 |
+
)
|
| 316 |
+
blender_output = output_buffer.getvalue()
|
| 317 |
+
if blender_output and retry == 2:
|
| 318 |
+
st.text(f"Blender output for scene {scene_idx + 1} (last 1000 chars):")
|
| 319 |
+
st.code(blender_output[-1000:] if len(blender_output) > 1000 else blender_output)
|
| 320 |
+
|
| 321 |
+
if base_scene and len(base_scene.get('objects', [])) > 0:
|
| 322 |
+
break
|
| 323 |
+
elif base_scene is None:
|
| 324 |
+
if retry == 2:
|
| 325 |
+
scene_error = f"generate_base_scene returned None - Blender may have failed (check output above)"
|
| 326 |
+
error_messages.append(f"Scene {scene_idx + 1}: {scene_error}")
|
| 327 |
+
elif len(base_scene.get('objects', [])) == 0:
|
| 328 |
+
if retry == 2:
|
| 329 |
+
scene_error = f"Scene has 0 objects - Blender may have hit max_retries (check output above)"
|
| 330 |
+
error_messages.append(f"Scene {scene_idx + 1}: {scene_error}")
|
| 331 |
+
except FileNotFoundError as e:
|
| 332 |
+
scene_error = f"Blender not found: {e}"
|
| 333 |
+
error_messages.append(f"Scene {scene_idx + 1}: {scene_error}")
|
| 334 |
+
blender_available = False
|
| 335 |
+
break
|
| 336 |
+
except Exception as e:
|
| 337 |
+
import traceback
|
| 338 |
+
scene_error = f"Error generating base scene: {str(e)}"
|
| 339 |
+
print(f"Error generating base scene (retry {retry + 1}/3): {e}")
|
| 340 |
+
print(f" Traceback: {traceback.format_exc()}")
|
| 341 |
+
if retry == 2:
|
| 342 |
+
full_error = f"Scene {scene_idx + 1}: {scene_error} (Blender path: {blender_path})"
|
| 343 |
+
error_messages.append(full_error)
|
| 344 |
+
blender_available = False
|
| 345 |
+
continue
|
| 346 |
+
else:
|
| 347 |
+
print(f"Scene {scene_idx + 1} (Blender not available)...")
|
| 348 |
+
base_scene = generate_fallback_scene(scene_num_objects, scene_idx)
|
| 349 |
+
|
| 350 |
+
if not base_scene or len(base_scene.get('objects', [])) == 0:
|
| 351 |
+
error_detail = f"Scene {scene_idx + 1}: Failed to generate"
|
| 352 |
+
if blender_available:
|
| 353 |
+
error_detail += f" (Blender was available at {blender_path} but returned empty scene)"
|
| 354 |
+
else:
|
| 355 |
+
error_detail += " (Blender not available, fallback scene also failed)"
|
| 356 |
+
print(f"Failed to generate scene {scene_idx + 1}")
|
| 357 |
+
print(f" Blender available: {blender_available}")
|
| 358 |
+
print(f" Blender path: {blender_path}")
|
| 359 |
+
print(f" Base scene: {base_scene is not None}")
|
| 360 |
+
if base_scene:
|
| 361 |
+
print(f" Objects in scene: {len(base_scene.get('objects', []))}")
|
| 362 |
+
error_messages.append(error_detail)
|
| 363 |
+
continue
|
| 364 |
+
|
| 365 |
+
successful_scenes += 1
|
| 366 |
+
|
| 367 |
+
counterfactuals = generate_counterfactuals(
|
| 368 |
+
base_scene,
|
| 369 |
+
num_counterfactuals=num_counterfactuals,
|
| 370 |
+
cf_types=cf_types,
|
| 371 |
+
same_cf_type=same_cf_type,
|
| 372 |
+
min_change_score=min_change_score,
|
| 373 |
+
max_cf_attempts=max_cf_attempts,
|
| 374 |
+
min_noise_level='light',
|
| 375 |
+
semantic_only=semantic_only,
|
| 376 |
+
negative_only=negative_only
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
scene_num = scene_idx + 1
|
| 380 |
+
scene_prefix = f"scene_{scene_num:04d}"
|
| 381 |
+
|
| 382 |
+
base_scene['cf_metadata'] = {
|
| 383 |
+
'variant': 'original',
|
| 384 |
+
'is_counterfactual': False,
|
| 385 |
+
'cf_index': None,
|
| 386 |
+
'cf_category': 'original',
|
| 387 |
+
'cf_type': None,
|
| 388 |
+
'cf_description': None,
|
| 389 |
+
'source_scene': scene_prefix,
|
| 390 |
+
}
|
| 391 |
+
original_scene_path = os.path.join(scenes_dir, f"{scene_prefix}_original.json")
|
| 392 |
+
save_scene(base_scene, original_scene_path)
|
| 393 |
+
|
| 394 |
+
for idx, cf in enumerate(counterfactuals):
|
| 395 |
+
cf_name = f"cf{idx+1}"
|
| 396 |
+
cf_scene = cf['scene']
|
| 397 |
+
cf_scene['cf_metadata'] = {
|
| 398 |
+
'variant': cf_name,
|
| 399 |
+
'is_counterfactual': True,
|
| 400 |
+
'cf_index': idx + 1,
|
| 401 |
+
'cf_category': cf.get('cf_category', 'unknown'),
|
| 402 |
+
'cf_type': cf.get('type', None),
|
| 403 |
+
'cf_description': cf.get('description', None),
|
| 404 |
+
'change_score': cf.get('change_score', None),
|
| 405 |
+
'change_attempts': cf.get('change_attempts', None),
|
| 406 |
+
'source_scene': scene_prefix,
|
| 407 |
+
}
|
| 408 |
+
cf_scene_path = os.path.join(scenes_dir, f"{scene_prefix}_{cf_name}.json")
|
| 409 |
+
save_scene(cf_scene, cf_scene_path)
|
| 410 |
+
|
| 411 |
+
render_success = 0
|
| 412 |
+
total_to_render = len(counterfactuals) + 1
|
| 413 |
+
|
| 414 |
+
if not skip_render:
|
| 415 |
+
if blender_path and blender_available:
|
| 416 |
+
original_image_path = os.path.join(images_dir, f"{scene_prefix}_original.png")
|
| 417 |
+
if render_scene(
|
| 418 |
+
blender_path,
|
| 419 |
+
original_scene_path,
|
| 420 |
+
original_image_path,
|
| 421 |
+
use_gpu=use_gpu,
|
| 422 |
+
samples=samples,
|
| 423 |
+
width=width,
|
| 424 |
+
height=height
|
| 425 |
+
):
|
| 426 |
+
render_success += 1
|
| 427 |
+
|
| 428 |
+
for idx, cf in enumerate(counterfactuals):
|
| 429 |
+
cf_name = f"cf{idx+1}"
|
| 430 |
+
cf_scene_path = os.path.join(scenes_dir, f"{scene_prefix}_{cf_name}.json")
|
| 431 |
+
cf_image_path = os.path.join(images_dir, f"{scene_prefix}_{cf_name}.png")
|
| 432 |
+
|
| 433 |
+
if render_scene(
|
| 434 |
+
blender_path,
|
| 435 |
+
cf_scene_path,
|
| 436 |
+
cf_image_path,
|
| 437 |
+
use_gpu=use_gpu,
|
| 438 |
+
samples=samples,
|
| 439 |
+
width=width,
|
| 440 |
+
height=height
|
| 441 |
+
):
|
| 442 |
+
render_success += 1
|
| 443 |
+
|
| 444 |
+
if render_success == total_to_render:
|
| 445 |
+
successful_renders += 1
|
| 446 |
+
else:
|
| 447 |
+
print("Blender not available - skipping image rendering. Scene JSON files will still be generated.")
|
| 448 |
+
|
| 449 |
+
csv_filename = 'image_mapping_with_questions.csv' if generate_questions else 'image_mapping.csv'
|
| 450 |
+
csv_path = os.path.join(output_dir, csv_filename)
|
| 451 |
+
|
| 452 |
+
try:
|
| 453 |
+
if generate_mapping_with_questions is not None:
|
| 454 |
+
generate_mapping_with_questions(
|
| 455 |
+
run_dir=output_dir,
|
| 456 |
+
csv_filename=csv_filename,
|
| 457 |
+
generate_questions=generate_questions,
|
| 458 |
+
with_links=False,
|
| 459 |
+
strict_question_validation=True
|
| 460 |
+
)
|
| 461 |
+
csv_created = os.path.exists(csv_path)
|
| 462 |
+
else:
|
| 463 |
+
csv_created = False
|
| 464 |
+
except Exception:
|
| 465 |
+
import traceback
|
| 466 |
+
traceback.print_exc()
|
| 467 |
+
csv_created = False
|
| 468 |
+
|
| 469 |
+
scene_files = list(Path(scenes_dir).glob("*.json")) if os.path.exists(scenes_dir) else []
|
| 470 |
+
image_files = list(Path(images_dir).glob("*.png")) if os.path.exists(images_dir) else []
|
| 471 |
+
|
| 472 |
+
statistics = {
|
| 473 |
+
'scenes_generated': successful_scenes,
|
| 474 |
+
'scenes_rendered': successful_renders,
|
| 475 |
+
'total_scene_files': len(scene_files),
|
| 476 |
+
'total_image_files': len(image_files),
|
| 477 |
+
'num_counterfactuals': num_counterfactuals,
|
| 478 |
+
'cf_types_used': cf_types if cf_types else 'default',
|
| 479 |
+
'csv_created': csv_created,
|
| 480 |
+
'csv_path': csv_path if csv_created else None
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 484 |
+
cwd = os.getcwd()
|
| 485 |
+
import shutil
|
| 486 |
+
import time
|
| 487 |
+
|
| 488 |
+
temp_output_dir = os.path.join(cwd, 'temp_output')
|
| 489 |
+
if os.path.exists(temp_output_dir):
|
| 490 |
+
for attempt in range(3):
|
| 491 |
+
try:
|
| 492 |
+
shutil.rmtree(temp_output_dir)
|
| 493 |
+
break
|
| 494 |
+
except Exception as e:
|
| 495 |
+
if attempt < 2:
|
| 496 |
+
time.sleep(0.3)
|
| 497 |
+
else:
|
| 498 |
+
print(f"Warning: Could not remove temp_output after 3 attempts: {e}")
|
| 499 |
+
|
| 500 |
+
render_patched_path = os.path.join(cwd, 'render_images_patched.py')
|
| 501 |
+
if os.path.exists(render_patched_path):
|
| 502 |
+
for attempt in range(3):
|
| 503 |
+
try:
|
| 504 |
+
time.sleep(0.2)
|
| 505 |
+
if os.path.exists(render_patched_path):
|
| 506 |
+
os.remove(render_patched_path)
|
| 507 |
+
break
|
| 508 |
+
except Exception as e:
|
| 509 |
+
if attempt < 2:
|
| 510 |
+
time.sleep(0.3)
|
| 511 |
+
else:
|
| 512 |
+
print(f"Warning: Could not remove render_images_patched.py after 3 attempts: {e}")
|
| 513 |
+
|
| 514 |
+
if successful_scenes == 0 and error_messages:
|
| 515 |
+
error_summary = "Scenes failed. Common reasons:\n"
|
| 516 |
+
error_summary += "- Blender is not installed or not in PATH\n"
|
| 517 |
+
error_summary += "- Blender executable not found\n"
|
| 518 |
+
error_summary += f"\nFirst error: {error_messages[0] if error_messages else 'Unknown error'}"
|
| 519 |
+
|
| 520 |
+
return {
|
| 521 |
+
'success': False,
|
| 522 |
+
'error': error_summary,
|
| 523 |
+
'num_scenes': successful_scenes,
|
| 524 |
+
'output_dir': output_dir,
|
| 525 |
+
'error_messages': error_messages
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
return {
|
| 529 |
+
'success': True,
|
| 530 |
+
'num_scenes': successful_scenes,
|
| 531 |
+
'output_dir': output_dir,
|
| 532 |
+
'statistics': statistics,
|
| 533 |
+
'error_messages': error_messages if error_messages else None
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
except Exception as e:
|
| 537 |
+
import traceback
|
| 538 |
+
error_msg = f"Error: {str(e)}\n{traceback.format_exc()}"
|
| 539 |
+
print(error_msg)
|
| 540 |
+
return {
|
| 541 |
+
'success': False,
|
| 542 |
+
'error': error_msg,
|
| 543 |
+
'num_scenes': successful_scenes,
|
| 544 |
+
'output_dir': output_dir,
|
| 545 |
+
'error_messages': error_messages if 'error_messages' in locals() else []
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
def main():
|
| 549 |
+
st.markdown('<p class="main-header">Counterfactual Image Generator</p>', unsafe_allow_html=True)
|
| 550 |
+
|
| 551 |
+
if 'output_dir' not in st.session_state:
|
| 552 |
+
st.session_state.output_dir = None
|
| 553 |
+
|
| 554 |
+
if 'generation_complete' not in st.session_state:
|
| 555 |
+
st.session_state.generation_complete = False
|
| 556 |
+
|
| 557 |
+
with st.sidebar:
|
| 558 |
+
st.header("Configuration")
|
| 559 |
+
|
| 560 |
+
st.subheader("Scene Settings")
|
| 561 |
+
num_scenes = st.number_input(
|
| 562 |
+
"Number of Scenes",
|
| 563 |
+
min_value=1,
|
| 564 |
+
max_value=10000,
|
| 565 |
+
value=5,
|
| 566 |
+
help="Number of scene sets to generate"
|
| 567 |
+
)
|
| 568 |
+
|
| 569 |
+
use_fixed_objects = st.checkbox("Use Fixed Number of Objects", value=True)
|
| 570 |
+
|
| 571 |
+
if use_fixed_objects:
|
| 572 |
+
num_objects = st.number_input(
|
| 573 |
+
"Number of Objects per Scene",
|
| 574 |
+
min_value=1,
|
| 575 |
+
max_value=15,
|
| 576 |
+
value=5,
|
| 577 |
+
help="Fixed number of objects per scene"
|
| 578 |
+
)
|
| 579 |
+
min_objects = None
|
| 580 |
+
max_objects = None
|
| 581 |
+
else:
|
| 582 |
+
num_objects = None
|
| 583 |
+
min_objects = st.number_input(
|
| 584 |
+
"Min Objects per Scene",
|
| 585 |
+
min_value=1,
|
| 586 |
+
max_value=15,
|
| 587 |
+
value=3,
|
| 588 |
+
help="Minimum objects per scene"
|
| 589 |
+
)
|
| 590 |
+
max_objects = st.number_input(
|
| 591 |
+
"Max Objects per Scene",
|
| 592 |
+
min_value=1,
|
| 593 |
+
max_value=15,
|
| 594 |
+
value=7,
|
| 595 |
+
help="Maximum objects per scene"
|
| 596 |
+
)
|
| 597 |
+
if min_objects > max_objects:
|
| 598 |
+
st.error("Min objects must be <= Max objects")
|
| 599 |
+
return
|
| 600 |
+
|
| 601 |
+
st.subheader("Counterfactual Settings")
|
| 602 |
+
num_counterfactuals = st.number_input(
|
| 603 |
+
"Number of Counterfactuals",
|
| 604 |
+
min_value=1,
|
| 605 |
+
max_value=10,
|
| 606 |
+
value=2,
|
| 607 |
+
help="Number of counterfactual variants per scene"
|
| 608 |
+
)
|
| 609 |
+
|
| 610 |
+
st.markdown("**Counterfactual Types**")
|
| 611 |
+
st.caption("Leave all unchecked to use default behavior (1 Image CF + 1 Negative CF)")
|
| 612 |
+
semantic_only = st.checkbox(
|
| 613 |
+
"Semantic only",
|
| 614 |
+
value=False,
|
| 615 |
+
help="Generate only Semantic/Image counterfactuals (Change Color, Add Object, etc.); no Negative CFs"
|
| 616 |
+
)
|
| 617 |
+
negative_only = st.checkbox(
|
| 618 |
+
"Negative only",
|
| 619 |
+
value=False,
|
| 620 |
+
help="Generate only Negative counterfactuals (Change Lighting, Add Noise, Occlusion Change, etc.); no Semantic CFs"
|
| 621 |
+
)
|
| 622 |
+
same_cf_type = st.checkbox(
|
| 623 |
+
"Same CF type for all",
|
| 624 |
+
value=False,
|
| 625 |
+
help="Use the same counterfactual type for every variant (first selected type, or one random if none selected)"
|
| 626 |
+
)
|
| 627 |
+
with st.expander("Image CFs (change answers)", expanded=True):
|
| 628 |
+
use_change_color = st.checkbox("Change Color", value=False)
|
| 629 |
+
use_change_shape = st.checkbox("Change Shape", value=False)
|
| 630 |
+
use_change_size = st.checkbox("Change Size", value=False)
|
| 631 |
+
use_change_material = st.checkbox("Change Material", value=False)
|
| 632 |
+
use_change_position = st.checkbox("Change Position", value=False)
|
| 633 |
+
use_add_object = st.checkbox("Add Object", value=False)
|
| 634 |
+
use_remove_object = st.checkbox("Remove Object", value=False)
|
| 635 |
+
use_replace_object = st.checkbox("Replace Object", value=False)
|
| 636 |
+
use_swap_attribute = st.checkbox("Swap Attribute", value=False)
|
| 637 |
+
use_relational_flip = st.checkbox("Relational Flip", value=False)
|
| 638 |
+
|
| 639 |
+
with st.expander("Negative CFs (don't change answers)", expanded=False):
|
| 640 |
+
use_change_background = st.checkbox("Change Background", value=False)
|
| 641 |
+
use_change_lighting = st.checkbox("Change Lighting", value=False)
|
| 642 |
+
use_add_noise = st.checkbox("Add Noise", value=False)
|
| 643 |
+
use_occlusion_change = st.checkbox("Occlusion Change", value=False)
|
| 644 |
+
use_apply_fisheye = st.checkbox("Apply Fisheye", value=False)
|
| 645 |
+
use_apply_blur = st.checkbox("Apply Blur", value=False)
|
| 646 |
+
use_apply_vignette = st.checkbox("Apply Vignette", value=False)
|
| 647 |
+
use_apply_chromatic_aberration = st.checkbox("Apply Chromatic Aberration", value=False)
|
| 648 |
+
|
| 649 |
+
with st.expander("Advanced Settings", expanded=False):
|
| 650 |
+
min_change_score = st.slider(
|
| 651 |
+
"Minimum Change Score",
|
| 652 |
+
min_value=0.5,
|
| 653 |
+
max_value=5.0,
|
| 654 |
+
value=1.0,
|
| 655 |
+
step=0.1,
|
| 656 |
+
help="Minimum heuristic change score for counterfactuals"
|
| 657 |
+
)
|
| 658 |
+
|
| 659 |
+
max_cf_attempts = st.number_input(
|
| 660 |
+
"Max CF Attempts",
|
| 661 |
+
min_value=1,
|
| 662 |
+
max_value=50,
|
| 663 |
+
value=10,
|
| 664 |
+
help="Maximum retries per counterfactual"
|
| 665 |
+
)
|
| 666 |
+
|
| 667 |
+
min_noise_level = st.selectbox(
|
| 668 |
+
"Min Noise Level (for add_noise CF)",
|
| 669 |
+
options=['light', 'medium', 'heavy'],
|
| 670 |
+
index=0,
|
| 671 |
+
help="Minimum noise level when using add_noise counterfactual"
|
| 672 |
+
)
|
| 673 |
+
|
| 674 |
+
st.markdown("---")
|
| 675 |
+
st.markdown("**Rendering Settings**")
|
| 676 |
+
|
| 677 |
+
use_gpu = st.checkbox("Use GPU Rendering", value=False)
|
| 678 |
+
use_gpu_int = 1 if use_gpu else 0
|
| 679 |
+
|
| 680 |
+
samples = st.number_input(
|
| 681 |
+
"Render Samples",
|
| 682 |
+
min_value=64,
|
| 683 |
+
max_value=2048,
|
| 684 |
+
value=512,
|
| 685 |
+
step=64,
|
| 686 |
+
help="Cycles sampling rate (higher = better quality, slower)"
|
| 687 |
+
)
|
| 688 |
+
|
| 689 |
+
image_width = st.number_input(
|
| 690 |
+
"Image Width",
|
| 691 |
+
min_value=160,
|
| 692 |
+
max_value=1920,
|
| 693 |
+
value=320,
|
| 694 |
+
step=80
|
| 695 |
+
)
|
| 696 |
+
|
| 697 |
+
image_height = st.number_input(
|
| 698 |
+
"Image Height",
|
| 699 |
+
min_value=120,
|
| 700 |
+
max_value=1080,
|
| 701 |
+
value=240,
|
| 702 |
+
step=60
|
| 703 |
+
)
|
| 704 |
+
|
| 705 |
+
st.markdown("**CSV Options**")
|
| 706 |
+
generate_questions = st.checkbox(
|
| 707 |
+
"Generate Questions in CSV",
|
| 708 |
+
value=False,
|
| 709 |
+
help="Include question and answer columns in the CSV file"
|
| 710 |
+
)
|
| 711 |
+
|
| 712 |
+
cf_types = []
|
| 713 |
+
if use_change_color:
|
| 714 |
+
cf_types.append('change_color')
|
| 715 |
+
if use_change_shape:
|
| 716 |
+
cf_types.append('change_shape')
|
| 717 |
+
if use_change_size:
|
| 718 |
+
cf_types.append('change_size')
|
| 719 |
+
if use_change_material:
|
| 720 |
+
cf_types.append('change_material')
|
| 721 |
+
if use_change_position:
|
| 722 |
+
cf_types.append('change_position')
|
| 723 |
+
if use_add_object:
|
| 724 |
+
cf_types.append('add_object')
|
| 725 |
+
if use_remove_object:
|
| 726 |
+
cf_types.append('remove_object')
|
| 727 |
+
if use_replace_object:
|
| 728 |
+
cf_types.append('replace_object')
|
| 729 |
+
if use_swap_attribute:
|
| 730 |
+
cf_types.append('swap_attribute')
|
| 731 |
+
if use_relational_flip:
|
| 732 |
+
cf_types.append('relational_flip')
|
| 733 |
+
if use_change_background:
|
| 734 |
+
cf_types.append('change_background')
|
| 735 |
+
if use_change_lighting:
|
| 736 |
+
cf_types.append('change_lighting')
|
| 737 |
+
if use_add_noise:
|
| 738 |
+
cf_types.append('add_noise')
|
| 739 |
+
if use_occlusion_change:
|
| 740 |
+
cf_types.append('occlusion_change')
|
| 741 |
+
if use_apply_fisheye:
|
| 742 |
+
cf_types.append('apply_fisheye')
|
| 743 |
+
if use_apply_blur:
|
| 744 |
+
cf_types.append('apply_blur')
|
| 745 |
+
if use_apply_vignette:
|
| 746 |
+
cf_types.append('apply_vignette')
|
| 747 |
+
if use_apply_chromatic_aberration:
|
| 748 |
+
cf_types.append('apply_chromatic_aberration')
|
| 749 |
+
|
| 750 |
+
if not cf_types:
|
| 751 |
+
cf_types = None
|
| 752 |
+
|
| 753 |
+
col1, col2 = st.columns([2, 1])
|
| 754 |
+
|
| 755 |
+
with col1:
|
| 756 |
+
st.header("Generate Counterfactual Images")
|
| 757 |
+
|
| 758 |
+
if st.button("Generate Counterfactual", use_container_width=True, key="generate_button"):
|
| 759 |
+
st.session_state.generation_complete = False
|
| 760 |
+
st.session_state.generating = True
|
| 761 |
+
|
| 762 |
+
if num_scenes < 1:
|
| 763 |
+
st.error("Please specify at least 1 scene to generate.")
|
| 764 |
+
return
|
| 765 |
+
|
| 766 |
+
if use_fixed_objects and num_objects < 1:
|
| 767 |
+
st.error("Please specify at least 1 object per scene.")
|
| 768 |
+
return
|
| 769 |
+
if not use_fixed_objects and (min_objects < 1 or max_objects < 1 or min_objects > max_objects):
|
| 770 |
+
st.error("Invalid min/max objects configuration.")
|
| 771 |
+
return
|
| 772 |
+
|
| 773 |
+
if os.path.exists('/tmp'):
|
| 774 |
+
base_dir = '/tmp'
|
| 775 |
+
else:
|
| 776 |
+
base_dir = tempfile.gettempdir()
|
| 777 |
+
|
| 778 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 779 |
+
output_dir = os.path.join(base_dir, f"counterfactual_output_{timestamp}")
|
| 780 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 781 |
+
st.session_state.output_dir = output_dir
|
| 782 |
+
|
| 783 |
+
import shutil
|
| 784 |
+
import time
|
| 785 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 786 |
+
cwd = os.getcwd()
|
| 787 |
+
|
| 788 |
+
temp_output_dir = os.path.join(cwd, 'temp_output')
|
| 789 |
+
if os.path.exists(temp_output_dir):
|
| 790 |
+
for attempt in range(3):
|
| 791 |
+
try:
|
| 792 |
+
shutil.rmtree(temp_output_dir)
|
| 793 |
+
break
|
| 794 |
+
except Exception as e:
|
| 795 |
+
if attempt < 2:
|
| 796 |
+
time.sleep(0.3)
|
| 797 |
+
else:
|
| 798 |
+
print(f"Warning: Could not remove temp_output after 3 attempts: {e}")
|
| 799 |
+
|
| 800 |
+
render_patched_path = os.path.join(cwd, 'render_images_patched.py')
|
| 801 |
+
if os.path.exists(render_patched_path):
|
| 802 |
+
for attempt in range(3):
|
| 803 |
+
try:
|
| 804 |
+
time.sleep(0.2)
|
| 805 |
+
if os.path.exists(render_patched_path):
|
| 806 |
+
os.remove(render_patched_path)
|
| 807 |
+
break
|
| 808 |
+
except Exception as e:
|
| 809 |
+
if attempt < 2:
|
| 810 |
+
time.sleep(0.3)
|
| 811 |
+
else:
|
| 812 |
+
print(f"Warning: Could not remove render_images_patched.py after 3 attempts: {e}")
|
| 813 |
+
|
| 814 |
+
try:
|
| 815 |
+
from pipeline import create_patched_render_script
|
| 816 |
+
create_patched_render_script()
|
| 817 |
+
except Exception as e:
|
| 818 |
+
st.warning(f"Could not create patched render script: {e}")
|
| 819 |
+
|
| 820 |
+
params = {
|
| 821 |
+
'num_scenes': num_scenes,
|
| 822 |
+
'num_objects': num_objects,
|
| 823 |
+
'num_counterfactuals': num_counterfactuals,
|
| 824 |
+
'cf_types': cf_types if cf_types else None,
|
| 825 |
+
'same_cf_type': same_cf_type,
|
| 826 |
+
'min_change_score': min_change_score,
|
| 827 |
+
'max_cf_attempts': max_cf_attempts,
|
| 828 |
+
'width': image_width,
|
| 829 |
+
'height': image_height,
|
| 830 |
+
'output_dir': output_dir
|
| 831 |
+
}
|
| 832 |
+
|
| 833 |
+
progress_bar = st.progress(0)
|
| 834 |
+
status_text = st.empty()
|
| 835 |
+
|
| 836 |
+
try:
|
| 837 |
+
if not PIPELINE_AVAILABLE:
|
| 838 |
+
st.error("Pipeline functions are not available. Please check your installation.")
|
| 839 |
+
return
|
| 840 |
+
|
| 841 |
+
status_text.text("Initializing generator...")
|
| 842 |
+
progress_bar.progress(10)
|
| 843 |
+
|
| 844 |
+
if use_fixed_objects:
|
| 845 |
+
status_text.text(f"Generating {num_scenes} scenes with {num_objects} objects each...")
|
| 846 |
+
else:
|
| 847 |
+
status_text.text(f"Generating {num_scenes} scenes with {min_objects}-{max_objects} objects each...")
|
| 848 |
+
progress_bar.progress(30)
|
| 849 |
+
|
| 850 |
+
result = generate_counterfactual_scenes(
|
| 851 |
+
num_scenes=num_scenes,
|
| 852 |
+
num_objects=num_objects,
|
| 853 |
+
min_objects=min_objects,
|
| 854 |
+
max_objects=max_objects,
|
| 855 |
+
num_counterfactuals=num_counterfactuals,
|
| 856 |
+
cf_types=cf_types,
|
| 857 |
+
same_cf_type=same_cf_type,
|
| 858 |
+
min_change_score=min_change_score,
|
| 859 |
+
max_cf_attempts=max_cf_attempts,
|
| 860 |
+
min_noise_level=min_noise_level,
|
| 861 |
+
output_dir=output_dir,
|
| 862 |
+
use_gpu=use_gpu_int,
|
| 863 |
+
samples=samples,
|
| 864 |
+
width=image_width,
|
| 865 |
+
height=image_height,
|
| 866 |
+
skip_render=False,
|
| 867 |
+
generate_questions=generate_questions
|
| 868 |
+
)
|
| 869 |
+
|
| 870 |
+
progress_bar.progress(80)
|
| 871 |
+
status_text.text("Preparing output...")
|
| 872 |
+
|
| 873 |
+
if result and result.get('success', False):
|
| 874 |
+
num_scenes_generated = result.get('num_scenes', 0)
|
| 875 |
+
|
| 876 |
+
if num_scenes_generated == 0:
|
| 877 |
+
st.warning("No scenes were created. Blender is required and is not available in this environment.")
|
| 878 |
+
st.info("**To use this application:**\n"
|
| 879 |
+
"1. Run it locally with Blender installed\n"
|
| 880 |
+
"2. Use the command-line `pipeline.py` script\n"
|
| 881 |
+
"3. Install Blender and ensure it's in your system PATH")
|
| 882 |
+
st.session_state.generation_complete = False
|
| 883 |
+
else:
|
| 884 |
+
st.session_state.generation_complete = True
|
| 885 |
+
progress_bar.progress(100)
|
| 886 |
+
status_text.text("Done.")
|
| 887 |
+
|
| 888 |
+
st.success(f"Successfully generated {num_scenes_generated} scene sets!")
|
| 889 |
+
st.info(f"Output directory: {output_dir}")
|
| 890 |
+
|
| 891 |
+
if 'statistics' in result and result['statistics'].get('csv_created'):
|
| 892 |
+
csv_path = result['statistics'].get('csv_path')
|
| 893 |
+
if csv_path:
|
| 894 |
+
st.success(f"CSV file created: `{os.path.basename(csv_path)}`")
|
| 895 |
+
|
| 896 |
+
if 'statistics' in result:
|
| 897 |
+
stats = result['statistics']
|
| 898 |
+
st.json(stats)
|
| 899 |
+
else:
|
| 900 |
+
error_msg = result.get('error', 'Unknown error occurred') if result else 'Failed'
|
| 901 |
+
st.error(f"Generation failed: {error_msg}")
|
| 902 |
+
|
| 903 |
+
if 'blender' in error_msg.lower() or 'Blender' in error_msg or result.get('num_scenes', 0) == 0:
|
| 904 |
+
st.warning("**Important:** This application requires Blender to generate scenes. Blender is not available on Hugging Face Spaces.")
|
| 905 |
+
st.info("**To use this application:**\n"
|
| 906 |
+
"1. Run it locally with Blender installed\n"
|
| 907 |
+
"2. Use the command-line `pipeline.py` script\n"
|
| 908 |
+
"3. Install Blender and ensure it's in your system PATH")
|
| 909 |
+
|
| 910 |
+
st.session_state.generation_complete = False
|
| 911 |
+
st.session_state.generating = False
|
| 912 |
+
|
| 913 |
+
except Exception as e:
|
| 914 |
+
st.error(f"Error during generation: {str(e)}")
|
| 915 |
+
st.exception(e)
|
| 916 |
+
st.session_state.generation_complete = False
|
| 917 |
+
st.session_state.generating = False
|
| 918 |
+
progress_bar.progress(0)
|
| 919 |
+
status_text.text("Failed")
|
| 920 |
+
|
| 921 |
+
with col2:
|
| 922 |
+
st.header("Output")
|
| 923 |
+
|
| 924 |
+
if st.session_state.generation_complete and st.session_state.output_dir:
|
| 925 |
+
output_dir = st.session_state.output_dir
|
| 926 |
+
|
| 927 |
+
if os.path.exists(output_dir):
|
| 928 |
+
images_dir = os.path.join(output_dir, 'images')
|
| 929 |
+
scenes_dir = os.path.join(output_dir, 'scenes')
|
| 930 |
+
|
| 931 |
+
scene_files = list(Path(scenes_dir).glob("*.json")) if os.path.exists(scenes_dir) else []
|
| 932 |
+
image_files = list(Path(images_dir).glob("*.png")) if os.path.exists(images_dir) else []
|
| 933 |
+
csv_files = list(Path(output_dir).rglob("*.csv"))
|
| 934 |
+
|
| 935 |
+
st.success("Complete!")
|
| 936 |
+
st.metric("Scene Files", len(scene_files))
|
| 937 |
+
st.metric("CSV Files", len(csv_files))
|
| 938 |
+
st.metric("Image Files", len(image_files))
|
| 939 |
+
|
| 940 |
+
if image_files:
|
| 941 |
+
st.markdown("---")
|
| 942 |
+
st.subheader("Generated Images")
|
| 943 |
+
|
| 944 |
+
def get_counterfactual_type_from_scene(scene_file):
|
| 945 |
+
try:
|
| 946 |
+
with open(scene_file, 'r') as f:
|
| 947 |
+
scene_data = json.load(f)
|
| 948 |
+
cf_metadata = scene_data.get('cf_metadata', {})
|
| 949 |
+
cf_type = cf_metadata.get('cf_type', '')
|
| 950 |
+
if cf_type:
|
| 951 |
+
return cf_type.replace('_', ' ').title()
|
| 952 |
+
except Exception as e:
|
| 953 |
+
pass
|
| 954 |
+
return "Counterfactual"
|
| 955 |
+
|
| 956 |
+
scene_sets = {}
|
| 957 |
+
for img_file in image_files:
|
| 958 |
+
filename = img_file.name
|
| 959 |
+
if filename.startswith('scene_'):
|
| 960 |
+
parts = filename.replace('.png', '').split('_')
|
| 961 |
+
if len(parts) >= 3:
|
| 962 |
+
scene_num = parts[1]
|
| 963 |
+
scene_type = parts[2]
|
| 964 |
+
|
| 965 |
+
if scene_num not in scene_sets:
|
| 966 |
+
scene_sets[scene_num] = {}
|
| 967 |
+
|
| 968 |
+
scene_sets[scene_num][scene_type] = {
|
| 969 |
+
'image_path': str(img_file),
|
| 970 |
+
'filename': filename
|
| 971 |
+
}
|
| 972 |
+
|
| 973 |
+
sorted_scenes = sorted(scene_sets.keys())[:3]
|
| 974 |
+
|
| 975 |
+
for scene_idx, scene_num in enumerate(sorted_scenes):
|
| 976 |
+
scene_data = scene_sets[scene_num]
|
| 977 |
+
|
| 978 |
+
if 'original' not in scene_data:
|
| 979 |
+
continue
|
| 980 |
+
|
| 981 |
+
st.markdown(f"### Scene {scene_num}")
|
| 982 |
+
|
| 983 |
+
cols = st.columns(3)
|
| 984 |
+
|
| 985 |
+
with cols[0]:
|
| 986 |
+
original = scene_data['original']
|
| 987 |
+
st.image(original['image_path'], use_container_width=True, caption="Original")
|
| 988 |
+
|
| 989 |
+
cf_count = 0
|
| 990 |
+
for cf_key in ['cf1', 'cf2']:
|
| 991 |
+
if cf_key in scene_data and cf_count < 2:
|
| 992 |
+
cf_data = scene_data[cf_key]
|
| 993 |
+
cf_scene_file = os.path.join(scenes_dir, cf_data['filename'].replace('.png', '.json'))
|
| 994 |
+
cf_type = get_counterfactual_type_from_scene(cf_scene_file) if os.path.exists(cf_scene_file) else f"Counterfactual {cf_count + 1}"
|
| 995 |
+
|
| 996 |
+
with cols[cf_count + 1]:
|
| 997 |
+
st.image(cf_data['image_path'], use_container_width=True, caption=cf_type)
|
| 998 |
+
|
| 999 |
+
cf_count += 1
|
| 1000 |
+
|
| 1001 |
+
if scene_idx < len(sorted_scenes) - 1:
|
| 1002 |
+
st.markdown("---")
|
| 1003 |
+
|
| 1004 |
+
st.markdown("---")
|
| 1005 |
+
st.subheader("Download Output")
|
| 1006 |
+
|
| 1007 |
+
zip_filename = f"counterfactual_output_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
|
| 1008 |
+
zip_path = os.path.join(tempfile.gettempdir(), zip_filename)
|
| 1009 |
+
|
| 1010 |
+
try:
|
| 1011 |
+
create_zip_file(output_dir, zip_path)
|
| 1012 |
+
|
| 1013 |
+
file_size = os.path.getsize(zip_path) / (1024 * 1024)
|
| 1014 |
+
|
| 1015 |
+
with open(zip_path, 'rb') as f:
|
| 1016 |
+
st.download_button(
|
| 1017 |
+
label=f"Download as ZIP ({file_size:.2f} MB)",
|
| 1018 |
+
data=f.read(),
|
| 1019 |
+
file_name=zip_filename,
|
| 1020 |
+
mime="application/zip",
|
| 1021 |
+
use_container_width=True
|
| 1022 |
+
)
|
| 1023 |
+
|
| 1024 |
+
with st.expander("Output Structure"):
|
| 1025 |
+
st.text(f"Output directory: {output_dir}")
|
| 1026 |
+
if scene_files:
|
| 1027 |
+
st.text(f"\nScene files: {len(scene_files)}")
|
| 1028 |
+
st.text("Sample files:")
|
| 1029 |
+
for f in scene_files[:5]:
|
| 1030 |
+
st.text(f" - {f.name}")
|
| 1031 |
+
if csv_files:
|
| 1032 |
+
st.text(f"\nCSV files: {len(csv_files)}")
|
| 1033 |
+
for f in csv_files:
|
| 1034 |
+
st.text(f" - {f.name}")
|
| 1035 |
+
if image_files:
|
| 1036 |
+
st.text(f"\nImage files: {len(image_files)}")
|
| 1037 |
+
st.text("Sample files:")
|
| 1038 |
+
for f in image_files[:5]:
|
| 1039 |
+
st.text(f" - {f.name}")
|
| 1040 |
+
|
| 1041 |
+
except Exception as e:
|
| 1042 |
+
st.error(f"Error creating zip file: {str(e)}")
|
| 1043 |
+
else:
|
| 1044 |
+
st.warning("Output directory not found.")
|
| 1045 |
+
else:
|
| 1046 |
+
st.info("Configure parameters and click 'Generate Counterfactual' to start.")
|
| 1047 |
+
|
| 1048 |
+
st.markdown("---")
|
| 1049 |
+
st.markdown(
|
| 1050 |
+
"<div style='text-align: center; color: #666; padding: 1rem;'>"
|
| 1051 |
+
"Counterfactual Image Tool | Built with Streamlit"
|
| 1052 |
+
"</div>",
|
| 1053 |
+
unsafe_allow_html=True
|
| 1054 |
+
)
|
| 1055 |
+
|
| 1056 |
+
if __name__ == "__main__":
|
| 1057 |
+
main()
|
| 1058 |
+
|
data/CoGenT_A.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cube": [
|
| 3 |
+
"gray", "blue", "brown", "yellow"
|
| 4 |
+
],
|
| 5 |
+
"cylinder": [
|
| 6 |
+
"red", "green", "purple", "cyan"
|
| 7 |
+
],
|
| 8 |
+
"sphere": [
|
| 9 |
+
"gray", "red", "blue", "green", "brown", "purple", "cyan", "yellow"
|
| 10 |
+
]
|
| 11 |
+
}
|
data/CoGenT_B.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cube": [
|
| 3 |
+
"red", "green", "purple", "cyan"
|
| 4 |
+
],
|
| 5 |
+
"cylinder": [
|
| 6 |
+
"gray", "blue", "brown", "yellow"
|
| 7 |
+
],
|
| 8 |
+
"sphere": [
|
| 9 |
+
"gray", "red", "blue", "green", "brown", "purple", "cyan", "yellow"
|
| 10 |
+
]
|
| 11 |
+
}
|
data/base_scene.blend
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e9a6a0a51f377064ae78e6568fdb1c5a6c97db4e674b29682e345d677607eb82
|
| 3 |
+
size 512864
|
data/materials/MyMetal.blend
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e47b6f920aaa4f5db0306eed01c309e0fcd933fdf2a8b6032a5fe5012268eca6
|
| 3 |
+
size 1822252
|
data/materials/Rubber.blend
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:05aa7d57452a4f083febe9ba34483c3c66201d2836b7281794f7ddbd9f9e63ff
|
| 3 |
+
size 1847100
|
data/properties.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"shapes": {
|
| 3 |
+
"cube": "SmoothCube_v2",
|
| 4 |
+
"sphere": "Sphere",
|
| 5 |
+
"cylinder": "SmoothCylinder"
|
| 6 |
+
},
|
| 7 |
+
"colors": {
|
| 8 |
+
"gray": [87, 87, 87],
|
| 9 |
+
"red": [173, 35, 35],
|
| 10 |
+
"blue": [42, 75, 215],
|
| 11 |
+
"green": [29, 105, 20],
|
| 12 |
+
"brown": [129, 74, 25],
|
| 13 |
+
"purple": [129, 38, 192],
|
| 14 |
+
"cyan": [41, 208, 208],
|
| 15 |
+
"yellow": [255, 238, 51]
|
| 16 |
+
},
|
| 17 |
+
"materials": {
|
| 18 |
+
"rubber": "Rubber",
|
| 19 |
+
"metal": "MyMetal"
|
| 20 |
+
},
|
| 21 |
+
"sizes": {
|
| 22 |
+
"large": 0.7,
|
| 23 |
+
"small": 0.35
|
| 24 |
+
}
|
| 25 |
+
}
|
data/shapes/SmoothCube_v2.blend
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a901f32efbc2afe33de6874163002439ce943697d6f4b43d0ad494eac72035ce
|
| 3 |
+
size 750148
|
data/shapes/SmoothCylinder.blend
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f5c4bf6b1f5e1469eb868b4e6be092ae2656b7b41d3a07e23d8487e616289bfa
|
| 3 |
+
size 3434292
|
data/shapes/Sphere.blend
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f35d20b842193ac0f3e49d4642c1ebbc999142ead5ecc298ed024118a5e5df98
|
| 3 |
+
size 1753684
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
streamlit-app:
|
| 5 |
+
build:
|
| 6 |
+
context: .
|
| 7 |
+
dockerfile: Dockerfile
|
| 8 |
+
container_name: counterfactual-generator
|
| 9 |
+
ports:
|
| 10 |
+
- "8501:8501"
|
| 11 |
+
volumes:
|
| 12 |
+
# Mount data directory if needed
|
| 13 |
+
- ./data:/app/data:ro
|
| 14 |
+
# Mount output directory for persistence
|
| 15 |
+
- ./output:/app/output
|
| 16 |
+
# Mount temp output directory
|
| 17 |
+
- ./temp_output:/app/temp_output
|
| 18 |
+
environment:
|
| 19 |
+
- PYTHONUNBUFFERED=1
|
| 20 |
+
restart: unless-stopped
|
| 21 |
+
healthcheck:
|
| 22 |
+
test: ["CMD", "python", "-c", "import streamlit; print('OK')"]
|
| 23 |
+
interval: 30s
|
| 24 |
+
timeout: 10s
|
| 25 |
+
retries: 3
|
| 26 |
+
start_period: 40s
|
generate_semantic_semibalanced_200.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import csv
|
| 2 |
+
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def main() -> None:
|
| 7 |
+
base_dir = Path(__file__).resolve().parent
|
| 8 |
+
|
| 9 |
+
# Input mapping file (question mapping)
|
| 10 |
+
mapping_path = base_dir / "output" / "200samples_mmib_rel" / "image_mapping_with_questions_200_rel.csv"
|
| 11 |
+
|
| 12 |
+
# Output directory and file for the semantic semibalanced 200 subset
|
| 13 |
+
out_dir = base_dir / "output" / "semantic_semibalanced_200"
|
| 14 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 15 |
+
out_csv = out_dir / "balanced_benchmark_200_semantic.csv"
|
| 16 |
+
|
| 17 |
+
if not mapping_path.is_file():
|
| 18 |
+
raise FileNotFoundError(f"Mapping CSV not found at {mapping_path}")
|
| 19 |
+
|
| 20 |
+
with mapping_path.open(newline="", encoding="utf-8") as f_in:
|
| 21 |
+
reader = csv.DictReader(f_in)
|
| 22 |
+
rows = list(reader)
|
| 23 |
+
fieldnames = reader.fieldnames
|
| 24 |
+
|
| 25 |
+
if fieldnames is None:
|
| 26 |
+
raise ValueError("No header found in mapping CSV.")
|
| 27 |
+
|
| 28 |
+
# We require that the original question's answer on the base image
|
| 29 |
+
# differs from the answer to the same (original) question on the cf1 image.
|
| 30 |
+
# Also drop rows where either of these answers is unknown.
|
| 31 |
+
required_cols = [
|
| 32 |
+
"original_image_answer_to_original_question",
|
| 33 |
+
"cf_image_answer_to_original_question",
|
| 34 |
+
]
|
| 35 |
+
for col in required_cols:
|
| 36 |
+
if col not in fieldnames:
|
| 37 |
+
raise KeyError(f"Expected column '{col}' not found in mapping CSV.")
|
| 38 |
+
|
| 39 |
+
filtered_rows = []
|
| 40 |
+
for row in rows:
|
| 41 |
+
base_ans = row["original_image_answer_to_original_question"]
|
| 42 |
+
cf1_ans = row["cf_image_answer_to_original_question"]
|
| 43 |
+
|
| 44 |
+
if base_ans.lower() == "unknown" or cf1_ans.lower() == "unknown":
|
| 45 |
+
continue
|
| 46 |
+
|
| 47 |
+
if base_ans != cf1_ans:
|
| 48 |
+
filtered_rows.append(row)
|
| 49 |
+
|
| 50 |
+
with out_csv.open("w", newline="", encoding="utf-8") as f_out:
|
| 51 |
+
writer = csv.DictWriter(f_out, fieldnames=fieldnames)
|
| 52 |
+
writer.writeheader()
|
| 53 |
+
writer.writerows(filtered_rows)
|
| 54 |
+
|
| 55 |
+
print(
|
| 56 |
+
f"Wrote {len(filtered_rows)} rows (from {len(rows)}) "
|
| 57 |
+
f"to {out_csv.relative_to(base_dir)}"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
if __name__ == "__main__":
|
| 62 |
+
main()
|
| 63 |
+
|
pipeline.py
ADDED
|
@@ -0,0 +1,2200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import argparse
|
| 5 |
+
import os
|
| 6 |
+
import subprocess
|
| 7 |
+
import csv
|
| 8 |
+
import random
|
| 9 |
+
import copy
|
| 10 |
+
import sys
|
| 11 |
+
import shutil
|
| 12 |
+
import time
|
| 13 |
+
import math
|
| 14 |
+
import glob
|
| 15 |
+
import zipfile
|
| 16 |
+
from datetime import datetime
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 20 |
+
sys.path.insert(0, os.path.join(script_dir, 'scripts'))
|
| 21 |
+
try:
|
| 22 |
+
from generate_questions_mapping import generate_mapping_with_questions
|
| 23 |
+
except ImportError:
|
| 24 |
+
generate_mapping_with_questions = None
|
| 25 |
+
|
| 26 |
+
def find_blender():
|
| 27 |
+
hf_paths = [
|
| 28 |
+
'/opt/blender/blender',
|
| 29 |
+
'/usr/local/bin/blender',
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
home = os.environ.get('HOME', os.path.expanduser('~'))
|
| 33 |
+
user_blender_paths = [
|
| 34 |
+
os.path.join(home, 'blender', 'blender'),
|
| 35 |
+
os.path.join(home, '.local', 'bin', 'blender'),
|
| 36 |
+
]
|
| 37 |
+
hf_paths.extend(user_blender_paths)
|
| 38 |
+
|
| 39 |
+
for path in hf_paths:
|
| 40 |
+
if os.path.exists(path):
|
| 41 |
+
return path
|
| 42 |
+
|
| 43 |
+
if sys.platform == 'win32':
|
| 44 |
+
common_paths = [
|
| 45 |
+
r"C:\Program Files\Blender Foundation\Blender 4.5\blender.exe",
|
| 46 |
+
r"C:\Program Files\Blender Foundation\Blender 4.3\blender.exe",
|
| 47 |
+
r"C:\Program Files\Blender Foundation\Blender 4.2\blender.exe",
|
| 48 |
+
]
|
| 49 |
+
for path in common_paths:
|
| 50 |
+
if os.path.exists(path):
|
| 51 |
+
return path
|
| 52 |
+
|
| 53 |
+
try:
|
| 54 |
+
env = os.environ.copy()
|
| 55 |
+
result = subprocess.run(['which', 'blender'], capture_output=True, timeout=5, env=env)
|
| 56 |
+
if result.returncode == 0:
|
| 57 |
+
blender_path = result.stdout.decode().strip()
|
| 58 |
+
if blender_path and os.path.exists(blender_path):
|
| 59 |
+
return blender_path
|
| 60 |
+
except:
|
| 61 |
+
pass
|
| 62 |
+
|
| 63 |
+
home = os.environ.get('HOME', os.path.expanduser('~'))
|
| 64 |
+
blender_default = os.path.join(home, 'blender', 'blender')
|
| 65 |
+
if os.path.exists(blender_default):
|
| 66 |
+
return blender_default
|
| 67 |
+
|
| 68 |
+
return 'blender'
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def create_run_directory(base_output_dir, run_name=None):
|
| 72 |
+
if run_name:
|
| 73 |
+
run_name = "".join(c for c in run_name if c.isalnum() or c in ('_', '-'))
|
| 74 |
+
run_dir = os.path.join(base_output_dir, run_name)
|
| 75 |
+
else:
|
| 76 |
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 77 |
+
run_dir = os.path.join(base_output_dir, timestamp)
|
| 78 |
+
|
| 79 |
+
os.makedirs(run_dir, exist_ok=True)
|
| 80 |
+
return run_dir
|
| 81 |
+
|
| 82 |
+
def save_checkpoint(checkpoint_file, completed_scenes):
|
| 83 |
+
with open(checkpoint_file, 'w') as f:
|
| 84 |
+
json.dump({'completed_scenes': completed_scenes}, f)
|
| 85 |
+
print(f" [CHECKPOINT] Saved: {len(completed_scenes)} scenes completed")
|
| 86 |
+
|
| 87 |
+
def load_checkpoint(checkpoint_file):
|
| 88 |
+
if os.path.exists(checkpoint_file):
|
| 89 |
+
with open(checkpoint_file, 'r') as f:
|
| 90 |
+
data = json.load(f)
|
| 91 |
+
return set(data.get('completed_scenes', []))
|
| 92 |
+
return set()
|
| 93 |
+
|
| 94 |
+
def get_completed_scenes_from_folder(images_dir):
|
| 95 |
+
if not os.path.exists(images_dir):
|
| 96 |
+
return set()
|
| 97 |
+
|
| 98 |
+
completed = set()
|
| 99 |
+
files = os.listdir(images_dir)
|
| 100 |
+
|
| 101 |
+
for f in files:
|
| 102 |
+
if f.startswith('scene_') and '_original.png' in f:
|
| 103 |
+
try:
|
| 104 |
+
scene_num = int(f.split('_')[1])
|
| 105 |
+
completed.add(scene_num)
|
| 106 |
+
except (IndexError, ValueError):
|
| 107 |
+
continue
|
| 108 |
+
|
| 109 |
+
return completed
|
| 110 |
+
|
| 111 |
+
def create_patched_utils():
|
| 112 |
+
return None
|
| 113 |
+
|
| 114 |
+
def create_patched_render_script():
|
| 115 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 116 |
+
render_path = os.path.join(current_dir, 'scripts', 'render.py')
|
| 117 |
+
|
| 118 |
+
with open(render_path, 'r', encoding='utf-8') as f:
|
| 119 |
+
original_content = f.read()
|
| 120 |
+
|
| 121 |
+
patched_content = original_content
|
| 122 |
+
|
| 123 |
+
import_patterns = [
|
| 124 |
+
'if INSIDE_BLENDER:\n import sys\n import os\n current_dir = os.path.dirname(os.path.abspath(__file__))\n if current_dir not in sys.path:\n sys.path.insert(0, current_dir)\n \n try:\n import utils',
|
| 125 |
+
'if INSIDE_BLENDER:\n try:\n import utils',
|
| 126 |
+
]
|
| 127 |
+
|
| 128 |
+
for pattern in import_patterns:
|
| 129 |
+
if pattern in patched_content:
|
| 130 |
+
import_start = patched_content.find(pattern)
|
| 131 |
+
if import_start != -1:
|
| 132 |
+
import_end = patched_content.find(' sys.exit(1)', import_start)
|
| 133 |
+
if import_end == -1:
|
| 134 |
+
import_end = patched_content.find('\nparser =', import_start)
|
| 135 |
+
if import_end != -1:
|
| 136 |
+
import_end = patched_content.find('\n', import_end)
|
| 137 |
+
patched_content = patched_content[:import_start] + patched_content[import_end:]
|
| 138 |
+
break
|
| 139 |
+
|
| 140 |
+
patched_content = patched_content.replace(
|
| 141 |
+
' render_args.tile_x = args.render_tile_size\n render_args.tile_y = args.render_tile_size',
|
| 142 |
+
' pass'
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
patched_content = patched_content.replace("render_args.engine = 'BLENDER_EEVEE'", "render_args.engine = 'CYCLES'")
|
| 146 |
+
|
| 147 |
+
patched_content = patched_content.replace(
|
| 148 |
+
"bpy.data.worlds['World'].cycles.sample_as_light = True",
|
| 149 |
+
"pass"
|
| 150 |
+
)
|
| 151 |
+
patched_content = patched_content.replace(
|
| 152 |
+
"bpy.context.scene.cycles.transparent_min_bounces = args.render_min_bounces",
|
| 153 |
+
"pass"
|
| 154 |
+
)
|
| 155 |
+
patched_content = patched_content.replace(
|
| 156 |
+
"bpy.context.scene.cycles.transparent_max_bounces = args.render_max_bounces",
|
| 157 |
+
"pass"
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
if "margins_good = True" in patched_content and "BROKEN MARGIN!" in patched_content:
|
| 161 |
+
old_margin_check = """ for direction_name in ['left', 'right', 'front', 'behind']:
|
| 162 |
+
direction_vec = scene_struct['directions'][direction_name]
|
| 163 |
+
assert direction_vec[2] == 0
|
| 164 |
+
margin = dx * direction_vec[0] + dy * direction_vec[1]
|
| 165 |
+
if 0 < margin < args.margin:
|
| 166 |
+
print(margin, args.margin, direction_name)
|
| 167 |
+
print('BROKEN MARGIN!')
|
| 168 |
+
margins_good = False
|
| 169 |
+
break"""
|
| 170 |
+
|
| 171 |
+
new_margin_check = """ pass"""
|
| 172 |
+
|
| 173 |
+
if old_margin_check in patched_content:
|
| 174 |
+
patched_content = patched_content.replace(old_margin_check, new_margin_check)
|
| 175 |
+
|
| 176 |
+
patched_content = patched_content.replace(
|
| 177 |
+
"parser.add_argument('--min_pixels_per_object', default=200, type=int,",
|
| 178 |
+
"parser.add_argument('--min_pixels_per_object', default=50, type=int,"
|
| 179 |
+
)
|
| 180 |
+
patched_content = patched_content.replace(
|
| 181 |
+
"parser.add_argument('--max_retries', default=50, type=int,",
|
| 182 |
+
"parser.add_argument('--max_retries', default=200, type=int,"
|
| 183 |
+
)
|
| 184 |
+
patched_content = patched_content.replace(
|
| 185 |
+
"parser.add_argument('--max_retries', default=100, type=int,",
|
| 186 |
+
"parser.add_argument('--max_retries', default=200, type=int,"
|
| 187 |
+
)
|
| 188 |
+
patched_content = patched_content.replace(
|
| 189 |
+
"parser.add_argument('--margin', default=0.4, type=float,",
|
| 190 |
+
"parser.add_argument('--margin', default=0.05, type=float,"
|
| 191 |
+
)
|
| 192 |
+
patched_content = patched_content.replace(
|
| 193 |
+
"parser.add_argument('--margin', default=0.2, type=float,",
|
| 194 |
+
"parser.add_argument('--margin', default=0.05, type=float,"
|
| 195 |
+
)
|
| 196 |
+
patched_content = patched_content.replace(
|
| 197 |
+
"parser.add_argument('--min_dist', default=0.25, type=float,",
|
| 198 |
+
"parser.add_argument('--min_dist', default=0.15, type=float,"
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
script_path = os.path.abspath('render_images_patched.py')
|
| 202 |
+
with open(script_path, 'w', encoding='utf-8') as f:
|
| 203 |
+
f.write(patched_content)
|
| 204 |
+
|
| 205 |
+
return script_path
|
| 206 |
+
|
| 207 |
+
def create_render_from_json_script():
|
| 208 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 209 |
+
render_path = os.path.join(current_dir, 'scripts', 'render.py')
|
| 210 |
+
return render_path
|
| 211 |
+
|
| 212 |
+
def generate_base_scene(num_objects, blender_path, scene_idx, temp_run_dir=None):
|
| 213 |
+
cwd = os.getcwd()
|
| 214 |
+
script_file = os.path.abspath(__file__)
|
| 215 |
+
current_dir = os.path.dirname(script_file)
|
| 216 |
+
temp_dir_to_clean = None
|
| 217 |
+
|
| 218 |
+
possible_dirs = [
|
| 219 |
+
current_dir,
|
| 220 |
+
cwd,
|
| 221 |
+
os.path.join(os.environ.get('HOME', ''), 'app'),
|
| 222 |
+
'/home/user/app',
|
| 223 |
+
os.path.dirname(cwd),
|
| 224 |
+
'/app',
|
| 225 |
+
]
|
| 226 |
+
|
| 227 |
+
data_dir = None
|
| 228 |
+
for possible_dir in possible_dirs:
|
| 229 |
+
if not possible_dir:
|
| 230 |
+
continue
|
| 231 |
+
test_data_dir = os.path.join(possible_dir, 'data')
|
| 232 |
+
if os.path.exists(test_data_dir) and os.path.exists(os.path.join(test_data_dir, 'base_scene.blend')):
|
| 233 |
+
data_dir = test_data_dir
|
| 234 |
+
break
|
| 235 |
+
|
| 236 |
+
if data_dir is None:
|
| 237 |
+
print(f" ERROR: Could not find data directory")
|
| 238 |
+
print(f" Searched in:")
|
| 239 |
+
for pd in possible_dirs:
|
| 240 |
+
if pd:
|
| 241 |
+
test_path = os.path.join(pd, 'data')
|
| 242 |
+
exists = os.path.exists(test_path)
|
| 243 |
+
print(f" - {test_path}: {'EXISTS' if exists else 'NOT FOUND'}")
|
| 244 |
+
if exists:
|
| 245 |
+
try:
|
| 246 |
+
contents = os.listdir(test_path)
|
| 247 |
+
print(f" Contents: {contents[:5]}")
|
| 248 |
+
except:
|
| 249 |
+
pass
|
| 250 |
+
print(f" Current working directory: {cwd}")
|
| 251 |
+
print(f" Script directory: {current_dir}")
|
| 252 |
+
print(f" Script file: {script_file}")
|
| 253 |
+
print(f" HOME: {os.environ.get('HOME', 'NOT SET')}")
|
| 254 |
+
return None
|
| 255 |
+
|
| 256 |
+
if temp_run_dir is None:
|
| 257 |
+
temp_run_dir = os.path.join(cwd, 'temp_output', f"run_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{scene_idx}")
|
| 258 |
+
temp_dir_to_clean = temp_run_dir
|
| 259 |
+
else:
|
| 260 |
+
temp_run_dir = os.path.join(cwd, 'temp_output', temp_run_dir) if not os.path.isabs(temp_run_dir) else temp_run_dir
|
| 261 |
+
|
| 262 |
+
temp_images_dir = os.path.join(temp_run_dir, 'images')
|
| 263 |
+
temp_scenes_dir = os.path.join(temp_run_dir, 'scenes')
|
| 264 |
+
|
| 265 |
+
os.makedirs(temp_images_dir, exist_ok=True)
|
| 266 |
+
os.makedirs(temp_scenes_dir, exist_ok=True)
|
| 267 |
+
|
| 268 |
+
try:
|
| 269 |
+
patched_script = create_patched_render_script()
|
| 270 |
+
|
| 271 |
+
if blender_path == 'blender':
|
| 272 |
+
blender_path = find_blender()
|
| 273 |
+
|
| 274 |
+
if not os.path.exists(blender_path) and blender_path != 'blender':
|
| 275 |
+
print(f" ERROR: Blender path does not exist: {blender_path}")
|
| 276 |
+
return None
|
| 277 |
+
|
| 278 |
+
base_scene = os.path.join(data_dir, 'base_scene.blend')
|
| 279 |
+
properties_json = os.path.join(data_dir, 'properties.json')
|
| 280 |
+
shape_dir = os.path.join(data_dir, 'shapes')
|
| 281 |
+
material_dir = os.path.join(data_dir, 'materials')
|
| 282 |
+
|
| 283 |
+
base_scene = os.path.abspath(base_scene)
|
| 284 |
+
properties_json = os.path.abspath(properties_json)
|
| 285 |
+
shape_dir = os.path.abspath(shape_dir)
|
| 286 |
+
material_dir = os.path.abspath(material_dir)
|
| 287 |
+
temp_images_dir = os.path.abspath(temp_images_dir)
|
| 288 |
+
temp_scenes_dir = os.path.abspath(temp_scenes_dir)
|
| 289 |
+
|
| 290 |
+
if not os.path.exists(base_scene):
|
| 291 |
+
print(f" ERROR: Base scene file not found: {base_scene}")
|
| 292 |
+
print(f" Data directory contents: {os.listdir(data_dir) if os.path.exists(data_dir) else 'N/A'}")
|
| 293 |
+
return None
|
| 294 |
+
if not os.path.exists(properties_json):
|
| 295 |
+
print(f" ERROR: Properties JSON not found: {properties_json}")
|
| 296 |
+
return None
|
| 297 |
+
if not os.path.exists(shape_dir):
|
| 298 |
+
print(f" ERROR: Shape directory not found: {shape_dir}")
|
| 299 |
+
if os.path.exists(data_dir):
|
| 300 |
+
print(f" Data directory contents: {os.listdir(data_dir)}")
|
| 301 |
+
return None
|
| 302 |
+
if not os.path.exists(material_dir):
|
| 303 |
+
print(f" ERROR: Material directory not found: {material_dir}")
|
| 304 |
+
return None
|
| 305 |
+
|
| 306 |
+
env = os.environ.copy()
|
| 307 |
+
|
| 308 |
+
cmd = [
|
| 309 |
+
blender_path, '--background', '-noaudio', '--python', patched_script, '--',
|
| 310 |
+
'--num_images', '1',
|
| 311 |
+
'--start_idx', str(scene_idx),
|
| 312 |
+
'--min_objects', str(num_objects),
|
| 313 |
+
'--max_objects', str(num_objects),
|
| 314 |
+
'--min_pixels_per_object', '50',
|
| 315 |
+
'--min_dist', '0.20',
|
| 316 |
+
'--margin', '0.2',
|
| 317 |
+
'--max_retries', '100',
|
| 318 |
+
'--output_image_dir', temp_images_dir,
|
| 319 |
+
'--output_scene_dir', temp_scenes_dir,
|
| 320 |
+
'--filename_prefix', f'SCENE_{scene_idx:04d}',
|
| 321 |
+
'--base_scene_blendfile', base_scene,
|
| 322 |
+
'--properties_json', properties_json,
|
| 323 |
+
'--shape_dir', shape_dir,
|
| 324 |
+
'--material_dir', material_dir,
|
| 325 |
+
]
|
| 326 |
+
|
| 327 |
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300, env=env, cwd=cwd)
|
| 328 |
+
|
| 329 |
+
if result.returncode != 0:
|
| 330 |
+
print(f"ERROR: Blender returned code {result.returncode}")
|
| 331 |
+
print(f" Blender path used: {blender_path}")
|
| 332 |
+
print(f" Command: {' '.join(cmd[:3])}...")
|
| 333 |
+
if result.stderr:
|
| 334 |
+
print(f" Error details (last 1000 chars): {result.stderr[-1000:]}")
|
| 335 |
+
if result.stdout:
|
| 336 |
+
error_lines = [line for line in result.stdout.split('\n') if 'error' in line.lower() or 'Error' in line or 'ERROR' in line or 'Traceback' in line]
|
| 337 |
+
if error_lines:
|
| 338 |
+
print(f" Error lines from stdout: {error_lines[-10:]}")
|
| 339 |
+
print(f" Full stdout (last 500 chars): {result.stdout[-500:]}")
|
| 340 |
+
return None
|
| 341 |
+
|
| 342 |
+
if not os.path.exists(temp_scenes_dir):
|
| 343 |
+
print(f"ERROR: temp run scenes directory does not exist")
|
| 344 |
+
print(f" Expected: {temp_scenes_dir}")
|
| 345 |
+
print(f" Current directory: {cwd}")
|
| 346 |
+
print(f" Temp run dir exists: {os.path.exists(temp_run_dir)}")
|
| 347 |
+
if os.path.exists(temp_run_dir):
|
| 348 |
+
print(f" Contents: {os.listdir(temp_run_dir)}")
|
| 349 |
+
return None
|
| 350 |
+
|
| 351 |
+
scene_file = os.path.join(temp_scenes_dir, f'SCENE_{scene_idx:04d}_new_{scene_idx:06d}.json')
|
| 352 |
+
if not os.path.exists(scene_file):
|
| 353 |
+
print(f" Scene file not found at expected path: {scene_file}")
|
| 354 |
+
if os.path.exists(temp_scenes_dir):
|
| 355 |
+
scene_files = os.listdir(temp_scenes_dir)
|
| 356 |
+
print(f" Available scene files ({len(scene_files)}): {scene_files[:10]}")
|
| 357 |
+
matching_files = [f for f in scene_files if f.startswith(f'SCENE_{scene_idx:04d}')]
|
| 358 |
+
print(f" Files matching SCENE_{scene_idx:04d}: {matching_files}")
|
| 359 |
+
if matching_files:
|
| 360 |
+
scene_file = os.path.join(temp_scenes_dir, matching_files[0])
|
| 361 |
+
print(f" Using: {scene_file}")
|
| 362 |
+
else:
|
| 363 |
+
print(f"ERROR: No matching scene file found for scene_idx {scene_idx}")
|
| 364 |
+
print(f" Checked for files starting with: SCENE_{scene_idx:04d}")
|
| 365 |
+
return None
|
| 366 |
+
else:
|
| 367 |
+
print(f"ERROR: temp run scenes directory does not exist: {temp_scenes_dir}")
|
| 368 |
+
return None
|
| 369 |
+
|
| 370 |
+
if not os.path.exists(scene_file):
|
| 371 |
+
print(f"ERROR: Scene file not found: {scene_file}")
|
| 372 |
+
return None
|
| 373 |
+
|
| 374 |
+
try:
|
| 375 |
+
with open(scene_file, 'r') as f:
|
| 376 |
+
scene = json.load(f)
|
| 377 |
+
except Exception as e:
|
| 378 |
+
print(f"ERROR: Failed to read scene file {scene_file}: {e}")
|
| 379 |
+
return None
|
| 380 |
+
|
| 381 |
+
if 'objects' not in scene:
|
| 382 |
+
print(f"ERROR: Scene file missing 'objects' key. Scene keys: {list(scene.keys())}")
|
| 383 |
+
print(f" Scene file contents (first 500 chars): {json.dumps(scene, indent=2)[:500]}")
|
| 384 |
+
return None
|
| 385 |
+
|
| 386 |
+
if len(scene['objects']) == 0:
|
| 387 |
+
print(f"WARNING: Scene has 0 objects")
|
| 388 |
+
print(f" This usually means Blender hit max_retries trying to place objects")
|
| 389 |
+
print(f" Scene file contents (first 1000 chars): {json.dumps(scene, indent=2)[:1000]}")
|
| 390 |
+
if result and result.stdout:
|
| 391 |
+
retry_lines = [line for line in result.stdout.split('\n') if 'retry' in line.lower() or 'Retry' in line or 'attempt' in line.lower()]
|
| 392 |
+
if retry_lines:
|
| 393 |
+
print(f" Retry-related output: {retry_lines[-5:]}")
|
| 394 |
+
return None
|
| 395 |
+
|
| 396 |
+
print(f" [OK] Generated scene with {len(scene['objects'])} objects")
|
| 397 |
+
return scene
|
| 398 |
+
finally:
|
| 399 |
+
if temp_dir_to_clean and os.path.exists(temp_dir_to_clean):
|
| 400 |
+
try:
|
| 401 |
+
shutil.rmtree(temp_dir_to_clean)
|
| 402 |
+
except Exception:
|
| 403 |
+
pass
|
| 404 |
+
|
| 405 |
+
def cf_change_color(scene):
|
| 406 |
+
cf_scene = copy.deepcopy(scene)
|
| 407 |
+
|
| 408 |
+
if len(cf_scene['objects']) == 0:
|
| 409 |
+
return cf_scene, "no change (0 objects)"
|
| 410 |
+
|
| 411 |
+
change_idx = random.randint(0, len(cf_scene['objects']) - 1)
|
| 412 |
+
obj = cf_scene['objects'][change_idx]
|
| 413 |
+
|
| 414 |
+
colors = ['gray', 'red', 'blue', 'green', 'brown', 'purple', 'cyan', 'yellow']
|
| 415 |
+
old_color = obj['color']
|
| 416 |
+
new_color = random.choice([c for c in colors if c != old_color])
|
| 417 |
+
obj['color'] = new_color
|
| 418 |
+
|
| 419 |
+
return cf_scene, f"changed {old_color} {obj['shape']} to {new_color}"
|
| 420 |
+
|
| 421 |
+
def cf_change_position(scene):
|
| 422 |
+
cf_scene = copy.deepcopy(scene)
|
| 423 |
+
|
| 424 |
+
if len(cf_scene['objects']) == 0:
|
| 425 |
+
return cf_scene, "no move (0 objects)"
|
| 426 |
+
|
| 427 |
+
move_idx = random.randint(0, len(cf_scene['objects']) - 1)
|
| 428 |
+
obj = cf_scene['objects'][move_idx]
|
| 429 |
+
old_coords = obj['3d_coords']
|
| 430 |
+
|
| 431 |
+
try:
|
| 432 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 433 |
+
with open(os.path.join(script_dir, 'data', 'properties.json'), 'r') as f:
|
| 434 |
+
properties = json.load(f)
|
| 435 |
+
size_mapping = properties['sizes']
|
| 436 |
+
except Exception:
|
| 437 |
+
size_mapping = {'small': 0.35, 'large': 0.7}
|
| 438 |
+
|
| 439 |
+
r = size_mapping.get(obj['size'], 0.5)
|
| 440 |
+
if obj['shape'] == 'cube':
|
| 441 |
+
r /= math.sqrt(2)
|
| 442 |
+
|
| 443 |
+
min_dist = 0.25
|
| 444 |
+
for attempt in range(100):
|
| 445 |
+
new_x = random.uniform(-3, 3)
|
| 446 |
+
new_y = random.uniform(-3, 3)
|
| 447 |
+
try:
|
| 448 |
+
dx0 = float(new_x) - float(old_coords[0])
|
| 449 |
+
dy0 = float(new_y) - float(old_coords[1])
|
| 450 |
+
if math.sqrt(dx0 * dx0 + dy0 * dy0) < 1.0:
|
| 451 |
+
continue
|
| 452 |
+
except Exception:
|
| 453 |
+
pass
|
| 454 |
+
|
| 455 |
+
collision = False
|
| 456 |
+
for other_idx, other_obj in enumerate(cf_scene['objects']):
|
| 457 |
+
if other_idx == move_idx:
|
| 458 |
+
continue
|
| 459 |
+
other_x, other_y, _ = other_obj['3d_coords']
|
| 460 |
+
other_r = size_mapping.get(other_obj['size'], 0.5)
|
| 461 |
+
if other_obj['shape'] == 'cube':
|
| 462 |
+
other_r /= math.sqrt(2)
|
| 463 |
+
dist = math.sqrt((new_x - other_x)**2 + (new_y - other_y)**2)
|
| 464 |
+
if dist < (r + other_r + min_dist):
|
| 465 |
+
collision = True
|
| 466 |
+
break
|
| 467 |
+
|
| 468 |
+
if not collision:
|
| 469 |
+
obj['3d_coords'] = [new_x, new_y, old_coords[2]]
|
| 470 |
+
obj['pixel_coords'] = [0, 0, 0]
|
| 471 |
+
return cf_scene, f"moved {obj['color']} {obj['shape']} from ({old_coords[0]:.1f},{old_coords[1]:.1f}) to ({new_x:.1f},{new_y:.1f})"
|
| 472 |
+
|
| 473 |
+
return cf_scene, f"no move (couldn't find collision-free position for {obj['color']} {obj['shape']})"
|
| 474 |
+
|
| 475 |
+
def cf_add_object(scene):
|
| 476 |
+
cf_scene = copy.deepcopy(scene)
|
| 477 |
+
|
| 478 |
+
shapes = ['cube', 'sphere', 'cylinder']
|
| 479 |
+
colors = ['gray', 'red', 'blue', 'green', 'brown', 'purple', 'cyan', 'yellow']
|
| 480 |
+
materials = ['metal', 'rubber']
|
| 481 |
+
sizes = ['small', 'large']
|
| 482 |
+
|
| 483 |
+
try:
|
| 484 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 485 |
+
with open(os.path.join(script_dir, 'data', 'properties.json'), 'r') as f:
|
| 486 |
+
properties = json.load(f)
|
| 487 |
+
size_mapping = properties['sizes']
|
| 488 |
+
except Exception:
|
| 489 |
+
size_mapping = {'small': 0.35, 'large': 0.7}
|
| 490 |
+
|
| 491 |
+
min_dist = 0.25
|
| 492 |
+
|
| 493 |
+
for attempt in range(100):
|
| 494 |
+
new_shape = random.choice(shapes)
|
| 495 |
+
new_size = random.choice(sizes)
|
| 496 |
+
new_r = size_mapping[new_size]
|
| 497 |
+
if new_shape == 'cube':
|
| 498 |
+
new_r /= math.sqrt(2)
|
| 499 |
+
|
| 500 |
+
new_x = random.uniform(-3, 3)
|
| 501 |
+
new_y = random.uniform(-3, 3)
|
| 502 |
+
|
| 503 |
+
collision = False
|
| 504 |
+
for other_obj in cf_scene['objects']:
|
| 505 |
+
other_x, other_y, _ = other_obj['3d_coords']
|
| 506 |
+
other_r = size_mapping.get(other_obj['size'], 0.5)
|
| 507 |
+
if other_obj['shape'] == 'cube':
|
| 508 |
+
other_r /= math.sqrt(2)
|
| 509 |
+
|
| 510 |
+
dist = math.sqrt((new_x - other_x)**2 + (new_y - other_y)**2)
|
| 511 |
+
if dist < (new_r + other_r + min_dist):
|
| 512 |
+
collision = True
|
| 513 |
+
break
|
| 514 |
+
|
| 515 |
+
if not collision:
|
| 516 |
+
new_obj = {
|
| 517 |
+
'shape': new_shape,
|
| 518 |
+
'color': random.choice(colors),
|
| 519 |
+
'material': random.choice(materials),
|
| 520 |
+
'size': new_size,
|
| 521 |
+
'3d_coords': [new_x, new_y, 0.35],
|
| 522 |
+
'rotation': random.uniform(0, 360),
|
| 523 |
+
'pixel_coords': [0, 0, 0]
|
| 524 |
+
}
|
| 525 |
+
cf_scene['objects'].append(new_obj)
|
| 526 |
+
return cf_scene, f"added {new_obj['color']} {new_obj['material']} {new_obj['shape']}"
|
| 527 |
+
|
| 528 |
+
return cf_scene, "no change (couldn't find collision-free position for new object)"
|
| 529 |
+
|
| 530 |
+
def cf_remove_object(scene):
|
| 531 |
+
cf_scene = copy.deepcopy(scene)
|
| 532 |
+
|
| 533 |
+
if len(cf_scene['objects']) <= 1:
|
| 534 |
+
return cf_scene, "no removal (1 or fewer objects)"
|
| 535 |
+
|
| 536 |
+
remove_idx = random.randint(0, len(cf_scene['objects']) - 1)
|
| 537 |
+
removed_obj = cf_scene['objects'].pop(remove_idx)
|
| 538 |
+
|
| 539 |
+
return cf_scene, f"removed {removed_obj['color']} {removed_obj['shape']}"
|
| 540 |
+
|
| 541 |
+
def cf_replace_object(scene):
|
| 542 |
+
cf_scene = copy.deepcopy(scene)
|
| 543 |
+
|
| 544 |
+
if len(cf_scene['objects']) == 0:
|
| 545 |
+
return cf_scene, "no replace (0 objects)"
|
| 546 |
+
|
| 547 |
+
replace_idx = random.randint(0, len(cf_scene['objects']) - 1)
|
| 548 |
+
old_obj = cf_scene['objects'][replace_idx]
|
| 549 |
+
old_desc = f"{old_obj['color']} {old_obj['shape']}"
|
| 550 |
+
|
| 551 |
+
shapes = ['cube', 'sphere', 'cylinder']
|
| 552 |
+
colors = ['gray', 'red', 'blue', 'green', 'brown', 'purple', 'cyan', 'yellow']
|
| 553 |
+
materials = ['metal', 'rubber']
|
| 554 |
+
sizes = ['small', 'large']
|
| 555 |
+
|
| 556 |
+
new_shape = random.choice([s for s in shapes if s != old_obj['shape']] or shapes)
|
| 557 |
+
new_color = random.choice([c for c in colors if c != old_obj['color']] or colors)
|
| 558 |
+
|
| 559 |
+
new_obj = {
|
| 560 |
+
'shape': new_shape,
|
| 561 |
+
'color': new_color,
|
| 562 |
+
'material': random.choice(materials),
|
| 563 |
+
'size': random.choice(sizes),
|
| 564 |
+
'3d_coords': old_obj['3d_coords'],
|
| 565 |
+
'rotation': random.uniform(0, 360),
|
| 566 |
+
'pixel_coords': [0, 0, 0]
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
cf_scene['objects'][replace_idx] = new_obj
|
| 570 |
+
|
| 571 |
+
return cf_scene, f"replaced {old_desc} with {new_color} {new_shape}"
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
def cf_swap_attribute(scene):
|
| 575 |
+
cf_scene = copy.deepcopy(scene)
|
| 576 |
+
|
| 577 |
+
if len(cf_scene['objects']) < 2:
|
| 578 |
+
return cf_scene, "no swap (fewer than 2 objects)"
|
| 579 |
+
# Only consider pairs that differ in at least one intrinsic attribute
|
| 580 |
+
# beyond color, so we avoid swapping attributes between identical
|
| 581 |
+
# (shape,size,material) objects where questions cannot distinguish them.
|
| 582 |
+
candidates = []
|
| 583 |
+
objs = cf_scene['objects']
|
| 584 |
+
n = len(objs)
|
| 585 |
+
for i in range(n):
|
| 586 |
+
for j in range(i + 1, n):
|
| 587 |
+
a = objs[i]
|
| 588 |
+
b = objs[j]
|
| 589 |
+
if a['color'] == b['color']:
|
| 590 |
+
continue
|
| 591 |
+
if a['shape'] == b['shape'] and a['size'] == b['size'] and a['material'] == b['material']:
|
| 592 |
+
continue
|
| 593 |
+
candidates.append((i, j))
|
| 594 |
+
if not candidates:
|
| 595 |
+
return cf_scene, "no swap (no suitable object pair with different shape/size/material)"
|
| 596 |
+
idx_a, idx_b = random.choice(candidates)
|
| 597 |
+
obj_a = objs[idx_a]
|
| 598 |
+
obj_b = objs[idx_b]
|
| 599 |
+
color_a, color_b = obj_a['color'], obj_b['color']
|
| 600 |
+
obj_a['color'] = color_b
|
| 601 |
+
obj_b['color'] = color_a
|
| 602 |
+
return cf_scene, f"swapped colors between {color_a} {obj_a['shape']} and {color_b} {obj_b['shape']}"
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
TARGET_OCCLUSION_COVERAGE = 0.6
|
| 606 |
+
# Lateral offset as fraction of combined radius: occluder placed to the SIDE of target
|
| 607 |
+
# so only part of target is hidden (partial occlusion), not fully behind occluder.
|
| 608 |
+
OCCLUSION_LATERAL_FRACTIONS = (0.35, 0.5, 0.65, 0.25, 0.45)
|
| 609 |
+
|
| 610 |
+
|
| 611 |
+
def cf_occlusion_change(scene):
|
| 612 |
+
cf_scene = copy.deepcopy(scene)
|
| 613 |
+
|
| 614 |
+
if len(cf_scene['objects']) < 2:
|
| 615 |
+
return cf_scene, "no occlusion (fewer than 2 objects)"
|
| 616 |
+
|
| 617 |
+
directions = cf_scene.get('directions', {})
|
| 618 |
+
front = directions.get('front', [0.75, -0.66, 0.0])
|
| 619 |
+
if len(front) < 2:
|
| 620 |
+
front = [0.75, -0.66]
|
| 621 |
+
left = directions.get('left', [-0.66, -0.75, 0.0])
|
| 622 |
+
if len(left) < 2:
|
| 623 |
+
left = [-0.66, -0.75]
|
| 624 |
+
|
| 625 |
+
try:
|
| 626 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 627 |
+
with open(os.path.join(script_dir, 'data', 'properties.json'), 'r') as f:
|
| 628 |
+
properties = json.load(f)
|
| 629 |
+
size_mapping = properties['sizes']
|
| 630 |
+
except Exception:
|
| 631 |
+
size_mapping = {'small': 0.35, 'large': 0.7}
|
| 632 |
+
|
| 633 |
+
def get_radius(obj):
|
| 634 |
+
r = size_mapping.get(obj['size'], 0.5)
|
| 635 |
+
if obj['shape'] == 'cube':
|
| 636 |
+
r /= math.sqrt(2)
|
| 637 |
+
return r
|
| 638 |
+
|
| 639 |
+
min_dist = 0.15
|
| 640 |
+
def is_valid_occlusion_pos(cf_scene, occluder_idx, target_idx, new_x, new_y, occluder_r):
|
| 641 |
+
for i, other in enumerate(cf_scene['objects']):
|
| 642 |
+
if i == occluder_idx:
|
| 643 |
+
continue
|
| 644 |
+
other_x, other_y, _ = other['3d_coords']
|
| 645 |
+
other_r = get_radius(other)
|
| 646 |
+
dist = math.sqrt((new_x - other_x)**2 + (new_y - other_y)**2)
|
| 647 |
+
if dist < (occluder_r + other_r + min_dist):
|
| 648 |
+
return False
|
| 649 |
+
return -2.8 <= new_x <= 2.8 and -2.8 <= new_y <= 2.8
|
| 650 |
+
|
| 651 |
+
fx, fy = float(front[0]), float(front[1])
|
| 652 |
+
fnorm = math.sqrt(fx * fx + fy * fy) or 1.0
|
| 653 |
+
lx, ly = float(left[0]), float(left[1])
|
| 654 |
+
lnorm = math.sqrt(lx * lx + ly * ly) or 1.0
|
| 655 |
+
# Forward offset: occluder between camera and target, but not jammed on top
|
| 656 |
+
forward_base = 0.25
|
| 657 |
+
forward_deltas = [0.0, 0.05, 0.1, 0.15, 0.2]
|
| 658 |
+
|
| 659 |
+
pairs = [(i, j) for i in range(len(cf_scene['objects'])) for j in range(len(cf_scene['objects'])) if i != j]
|
| 660 |
+
random.shuffle(pairs)
|
| 661 |
+
|
| 662 |
+
for occluder_idx, target_idx in pairs:
|
| 663 |
+
occluder = cf_scene['objects'][occluder_idx]
|
| 664 |
+
target = cf_scene['objects'][target_idx]
|
| 665 |
+
tx, ty, tz = target['3d_coords']
|
| 666 |
+
oz = occluder['3d_coords'][2]
|
| 667 |
+
occluder_r = get_radius(occluder)
|
| 668 |
+
target_r = get_radius(target)
|
| 669 |
+
combined = occluder_r + target_r + min_dist
|
| 670 |
+
# Lateral offset so occluder is to the side → partial overlap in camera view
|
| 671 |
+
for lateral_frac in OCCLUSION_LATERAL_FRACTIONS:
|
| 672 |
+
lateral = lateral_frac * combined
|
| 673 |
+
for sign in (1, -1):
|
| 674 |
+
lat_x = (lx / lnorm) * (lateral * sign)
|
| 675 |
+
lat_y = (ly / lnorm) * (lateral * sign)
|
| 676 |
+
for fdelta in forward_deltas:
|
| 677 |
+
forward = forward_base + fdelta
|
| 678 |
+
new_x = tx + (fx / fnorm) * forward + lat_x
|
| 679 |
+
new_y = ty + (fy / fnorm) * forward + lat_y
|
| 680 |
+
if is_valid_occlusion_pos(cf_scene, occluder_idx, target_idx, new_x, new_y, occluder_r):
|
| 681 |
+
occluder['3d_coords'] = [new_x, new_y, oz]
|
| 682 |
+
occluder['pixel_coords'] = [0, 0, 0]
|
| 683 |
+
return cf_scene, f"moved {occluder['color']} {occluder['shape']} to partially occlude {target['color']} {target['shape']}"
|
| 684 |
+
|
| 685 |
+
return cf_scene, "no occlusion (couldn't find valid position)"
|
| 686 |
+
|
| 687 |
+
|
| 688 |
+
def cf_relational_flip(scene):
|
| 689 |
+
cf_scene = copy.deepcopy(scene)
|
| 690 |
+
|
| 691 |
+
if len(cf_scene['objects']) < 2:
|
| 692 |
+
return cf_scene, "no flip (fewer than 2 objects)"
|
| 693 |
+
|
| 694 |
+
directions = cf_scene.get('directions', {})
|
| 695 |
+
left_vec = directions.get('left', [-0.66, -0.75, 0.0])
|
| 696 |
+
if len(left_vec) < 2:
|
| 697 |
+
left_vec = [-0.66, -0.75]
|
| 698 |
+
|
| 699 |
+
try:
|
| 700 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 701 |
+
with open(os.path.join(script_dir, 'data', 'properties.json'), 'r') as f:
|
| 702 |
+
properties = json.load(f)
|
| 703 |
+
size_mapping = properties['sizes']
|
| 704 |
+
except Exception:
|
| 705 |
+
size_mapping = {'small': 0.35, 'large': 0.7}
|
| 706 |
+
|
| 707 |
+
def get_radius(obj):
|
| 708 |
+
r = size_mapping.get(obj['size'], 0.5)
|
| 709 |
+
if obj['shape'] == 'cube':
|
| 710 |
+
r /= math.sqrt(2)
|
| 711 |
+
return r
|
| 712 |
+
|
| 713 |
+
def is_valid_pos(cf_scene, a_idx, new_x, new_y, r_a, min_dist=0.12):
|
| 714 |
+
for i, other in enumerate(cf_scene['objects']):
|
| 715 |
+
if i == a_idx:
|
| 716 |
+
continue
|
| 717 |
+
ox, oy, _ = other['3d_coords']
|
| 718 |
+
other_r = get_radius(other)
|
| 719 |
+
dist = math.sqrt((new_x - ox)**2 + (new_y - oy)**2)
|
| 720 |
+
if dist < (r_a + other_r + min_dist):
|
| 721 |
+
return False
|
| 722 |
+
return -2.8 <= new_x <= 2.8 and -2.8 <= new_y <= 2.8
|
| 723 |
+
|
| 724 |
+
relationships = cf_scene.get('relationships', {})
|
| 725 |
+
left_of = relationships.get('left', [])
|
| 726 |
+
right_of = relationships.get('right', [])
|
| 727 |
+
|
| 728 |
+
candidates = []
|
| 729 |
+
for b_idx in range(len(cf_scene['objects'])):
|
| 730 |
+
for a_idx in left_of[b_idx] if b_idx < len(left_of) else []:
|
| 731 |
+
if a_idx != b_idx and a_idx < len(cf_scene['objects']):
|
| 732 |
+
candidates.append((a_idx, b_idx, 'left'))
|
| 733 |
+
for a_idx in right_of[b_idx] if b_idx < len(right_of) else []:
|
| 734 |
+
if a_idx != b_idx and a_idx < len(cf_scene['objects']):
|
| 735 |
+
candidates.append((a_idx, b_idx, 'right'))
|
| 736 |
+
|
| 737 |
+
if not candidates:
|
| 738 |
+
lx, ly = float(left_vec[0]), float(left_vec[1])
|
| 739 |
+
for a_idx in range(len(cf_scene['objects'])):
|
| 740 |
+
for b_idx in range(len(cf_scene['objects'])):
|
| 741 |
+
if a_idx == b_idx:
|
| 742 |
+
continue
|
| 743 |
+
ax_a, ay_a, _ = cf_scene['objects'][a_idx]['3d_coords']
|
| 744 |
+
bx_b, by_b, _ = cf_scene['objects'][b_idx]['3d_coords']
|
| 745 |
+
dx, dy = ax_a - bx_b, ay_a - by_b
|
| 746 |
+
dot = dx * lx + dy * ly
|
| 747 |
+
if abs(dot) > 0.2:
|
| 748 |
+
side = 'left' if dot > 0 else 'right'
|
| 749 |
+
candidates.append((a_idx, b_idx, side))
|
| 750 |
+
|
| 751 |
+
if not candidates:
|
| 752 |
+
return cf_scene, "no flip (no clear left/right relationships)"
|
| 753 |
+
|
| 754 |
+
random.shuffle(candidates)
|
| 755 |
+
lx, ly = float(left_vec[0]), float(left_vec[1])
|
| 756 |
+
|
| 757 |
+
for a_idx, b_idx, side in candidates:
|
| 758 |
+
obj_a = cf_scene['objects'][a_idx]
|
| 759 |
+
obj_b = cf_scene['objects'][b_idx]
|
| 760 |
+
ax, ay, az = obj_a['3d_coords']
|
| 761 |
+
bx, by, bz = obj_b['3d_coords']
|
| 762 |
+
r_a = get_radius(obj_a)
|
| 763 |
+
|
| 764 |
+
dx, dy = ax - bx, ay - by
|
| 765 |
+
dot_left = dx * lx + dy * ly
|
| 766 |
+
ref_dx = dx - 2 * dot_left * lx
|
| 767 |
+
ref_dy = dy - 2 * dot_left * ly
|
| 768 |
+
|
| 769 |
+
for scale in [1.0, 0.9, 0.8, 0.7, 0.85, 0.75]:
|
| 770 |
+
new_x = bx + scale * ref_dx
|
| 771 |
+
new_y = by + scale * ref_dy
|
| 772 |
+
if is_valid_pos(cf_scene, a_idx, new_x, new_y, r_a):
|
| 773 |
+
obj_a['3d_coords'] = [new_x, new_y, az]
|
| 774 |
+
obj_a['pixel_coords'] = [0, 0, 0]
|
| 775 |
+
new_side = "right" if side == "left" else "left"
|
| 776 |
+
return cf_scene, f"moved {obj_a['color']} {obj_a['shape']} from {side} of {obj_b['color']} {obj_b['shape']} to {new_side}"
|
| 777 |
+
|
| 778 |
+
return cf_scene, "no flip (couldn't find collision-free position)"
|
| 779 |
+
|
| 780 |
+
|
| 781 |
+
def cf_change_background(scene):
|
| 782 |
+
cf_scene = copy.deepcopy(scene)
|
| 783 |
+
|
| 784 |
+
background_colors = ['gray', 'blue', 'green', 'brown', 'purple', 'orange', 'white', 'dark_gray']
|
| 785 |
+
current_bg = cf_scene.get('background_color', 'default')
|
| 786 |
+
|
| 787 |
+
alternatives = [c for c in background_colors if c != current_bg]
|
| 788 |
+
new_background = random.choice(alternatives) if alternatives else 'gray'
|
| 789 |
+
|
| 790 |
+
cf_scene['background_color'] = new_background
|
| 791 |
+
|
| 792 |
+
return cf_scene, f"changed background to {new_background}"
|
| 793 |
+
|
| 794 |
+
def cf_change_shape(scene):
|
| 795 |
+
cf_scene = copy.deepcopy(scene)
|
| 796 |
+
|
| 797 |
+
if len(cf_scene['objects']) == 0:
|
| 798 |
+
return cf_scene, "no change (0 objects)"
|
| 799 |
+
|
| 800 |
+
change_idx = random.randint(0, len(cf_scene['objects']) - 1)
|
| 801 |
+
obj = cf_scene['objects'][change_idx]
|
| 802 |
+
|
| 803 |
+
shapes = ['cube', 'sphere', 'cylinder']
|
| 804 |
+
old_shape = obj['shape']
|
| 805 |
+
new_shape = random.choice([s for s in shapes if s != old_shape])
|
| 806 |
+
obj['shape'] = new_shape
|
| 807 |
+
|
| 808 |
+
return cf_scene, f"changed {obj['color']} {old_shape} to {new_shape}"
|
| 809 |
+
|
| 810 |
+
def cf_change_size(scene):
|
| 811 |
+
cf_scene = copy.deepcopy(scene)
|
| 812 |
+
|
| 813 |
+
if len(cf_scene['objects']) == 0:
|
| 814 |
+
return cf_scene, "no change (0 objects)"
|
| 815 |
+
|
| 816 |
+
change_idx = random.randint(0, len(cf_scene['objects']) - 1)
|
| 817 |
+
obj = cf_scene['objects'][change_idx]
|
| 818 |
+
|
| 819 |
+
old_size = obj['size']
|
| 820 |
+
new_size = 'large' if old_size == 'small' else 'small'
|
| 821 |
+
obj['size'] = new_size
|
| 822 |
+
|
| 823 |
+
return cf_scene, f"changed {obj['color']} {obj['shape']} from {old_size} to {new_size}"
|
| 824 |
+
|
| 825 |
+
def cf_change_material(scene):
|
| 826 |
+
cf_scene = copy.deepcopy(scene)
|
| 827 |
+
|
| 828 |
+
if len(cf_scene['objects']) == 0:
|
| 829 |
+
return cf_scene, "no change (0 objects)"
|
| 830 |
+
|
| 831 |
+
change_idx = random.randint(0, len(cf_scene['objects']) - 1)
|
| 832 |
+
obj = cf_scene['objects'][change_idx]
|
| 833 |
+
|
| 834 |
+
old_material = obj['material']
|
| 835 |
+
new_material = 'rubber' if old_material == 'metal' else 'metal'
|
| 836 |
+
obj['material'] = new_material
|
| 837 |
+
|
| 838 |
+
return cf_scene, f"changed {obj['color']} {obj['shape']} from {old_material} to {new_material}"
|
| 839 |
+
|
| 840 |
+
def cf_change_lighting(scene):
|
| 841 |
+
cf_scene = copy.deepcopy(scene)
|
| 842 |
+
|
| 843 |
+
lighting_conditions = ['bright', 'dim', 'warm', 'cool', 'dramatic']
|
| 844 |
+
current_lighting = cf_scene.get('lighting', 'default')
|
| 845 |
+
|
| 846 |
+
alternatives = [l for l in lighting_conditions if l != current_lighting]
|
| 847 |
+
new_lighting = random.choice(alternatives) if alternatives else 'bright'
|
| 848 |
+
|
| 849 |
+
cf_scene['lighting'] = new_lighting
|
| 850 |
+
|
| 851 |
+
return cf_scene, f"changed lighting to {new_lighting}"
|
| 852 |
+
|
| 853 |
+
def cf_add_noise(scene, min_noise_level='light'):
|
| 854 |
+
cf_scene = copy.deepcopy(scene)
|
| 855 |
+
|
| 856 |
+
noise_levels = ['light', 'medium', 'heavy']
|
| 857 |
+
noise_weights = {'light': 0, 'medium': 1, 'heavy': 2}
|
| 858 |
+
|
| 859 |
+
min_weight = noise_weights.get(min_noise_level, 0)
|
| 860 |
+
valid_levels = [n for n in noise_levels if noise_weights[n] >= min_weight]
|
| 861 |
+
|
| 862 |
+
current = cf_scene.get('noise_level', None)
|
| 863 |
+
choices = [n for n in valid_levels if n != current] if current in valid_levels else valid_levels
|
| 864 |
+
|
| 865 |
+
if not choices:
|
| 866 |
+
choices = [min_noise_level] if min_noise_level in noise_levels else ['medium']
|
| 867 |
+
|
| 868 |
+
cf_scene['noise_level'] = random.choice(choices)
|
| 869 |
+
|
| 870 |
+
return cf_scene, f"added {cf_scene['noise_level']} noise (min: {min_noise_level})"
|
| 871 |
+
|
| 872 |
+
def cf_apply_fisheye(scene):
|
| 873 |
+
cf_scene = copy.deepcopy(scene)
|
| 874 |
+
|
| 875 |
+
current_filter = cf_scene.get('filter_type', None)
|
| 876 |
+
if current_filter == 'fisheye':
|
| 877 |
+
filter_strength = random.uniform(0.8, 1.2)
|
| 878 |
+
else:
|
| 879 |
+
filter_strength = random.uniform(0.8, 1.2)
|
| 880 |
+
|
| 881 |
+
cf_scene['filter_type'] = 'fisheye'
|
| 882 |
+
cf_scene['filter_strength'] = filter_strength
|
| 883 |
+
|
| 884 |
+
return cf_scene, f"applied fisheye filter (strength: {filter_strength:.2f})"
|
| 885 |
+
|
| 886 |
+
def cf_apply_blur(scene):
|
| 887 |
+
cf_scene = copy.deepcopy(scene)
|
| 888 |
+
|
| 889 |
+
current_filter = cf_scene.get('filter_type', None)
|
| 890 |
+
if current_filter == 'blur':
|
| 891 |
+
filter_strength = random.uniform(8.0, 15.0)
|
| 892 |
+
else:
|
| 893 |
+
filter_strength = random.uniform(8.0, 15.0)
|
| 894 |
+
|
| 895 |
+
cf_scene['filter_type'] = 'blur'
|
| 896 |
+
cf_scene['filter_strength'] = filter_strength
|
| 897 |
+
|
| 898 |
+
return cf_scene, f"applied blur filter (strength: {filter_strength:.2f})"
|
| 899 |
+
|
| 900 |
+
def cf_apply_vignette(scene):
|
| 901 |
+
cf_scene = copy.deepcopy(scene)
|
| 902 |
+
|
| 903 |
+
current_filter = cf_scene.get('filter_type', None)
|
| 904 |
+
if current_filter == 'vignette':
|
| 905 |
+
filter_strength = random.uniform(3.0, 5.0)
|
| 906 |
+
else:
|
| 907 |
+
filter_strength = random.uniform(3.0, 5.0)
|
| 908 |
+
|
| 909 |
+
cf_scene['filter_type'] = 'vignette'
|
| 910 |
+
cf_scene['filter_strength'] = filter_strength
|
| 911 |
+
|
| 912 |
+
return cf_scene, f"applied vignette filter (strength: {filter_strength:.2f})"
|
| 913 |
+
|
| 914 |
+
def cf_apply_chromatic_aberration(scene):
|
| 915 |
+
cf_scene = copy.deepcopy(scene)
|
| 916 |
+
|
| 917 |
+
current_filter = cf_scene.get('filter_type', None)
|
| 918 |
+
if current_filter == 'chromatic_aberration':
|
| 919 |
+
filter_strength = random.uniform(1.0, 4.0)
|
| 920 |
+
else:
|
| 921 |
+
filter_strength = random.uniform(1.0, 4.0)
|
| 922 |
+
|
| 923 |
+
cf_scene['filter_type'] = 'chromatic_aberration'
|
| 924 |
+
cf_scene['filter_strength'] = filter_strength
|
| 925 |
+
|
| 926 |
+
return cf_scene, f"applied chromatic aberration filter (strength: {filter_strength:.2f})"
|
| 927 |
+
|
| 928 |
+
IMAGE_COUNTERFACTUALS = {
|
| 929 |
+
'change_color': cf_change_color,
|
| 930 |
+
'change_shape': cf_change_shape,
|
| 931 |
+
'change_size': cf_change_size,
|
| 932 |
+
'change_material': cf_change_material,
|
| 933 |
+
'change_position': cf_change_position,
|
| 934 |
+
'add_object': cf_add_object,
|
| 935 |
+
'remove_object': cf_remove_object,
|
| 936 |
+
'replace_object': cf_replace_object,
|
| 937 |
+
'swap_attribute': cf_swap_attribute,
|
| 938 |
+
'relational_flip': cf_relational_flip,
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
NEGATIVE_COUNTERFACTUALS = {
|
| 942 |
+
'change_background': cf_change_background,
|
| 943 |
+
'change_lighting': cf_change_lighting,
|
| 944 |
+
'add_noise': cf_add_noise,
|
| 945 |
+
'apply_fisheye': cf_apply_fisheye,
|
| 946 |
+
'apply_blur': cf_apply_blur,
|
| 947 |
+
'apply_vignette': cf_apply_vignette,
|
| 948 |
+
'apply_chromatic_aberration': cf_apply_chromatic_aberration,
|
| 949 |
+
'occlusion_change': cf_occlusion_change,
|
| 950 |
+
}
|
| 951 |
+
|
| 952 |
+
DEFAULT_NEGATIVE_CF_TYPES = [k for k in NEGATIVE_COUNTERFACTUALS if k != 'apply_fisheye']
|
| 953 |
+
|
| 954 |
+
COUNTERFACTUAL_TYPES = {**IMAGE_COUNTERFACTUALS, **NEGATIVE_COUNTERFACTUALS}
|
| 955 |
+
|
| 956 |
+
def generate_counterfactuals(scene, num_counterfactuals=2, cf_types=None, same_cf_type=False, min_change_score=1.0, max_cf_attempts=10, min_noise_level='light', semantic_only=False, negative_only=False):
|
| 957 |
+
counterfactuals = []
|
| 958 |
+
|
| 959 |
+
def compute_change_score(original_scene, cf_scene):
|
| 960 |
+
score = 0.0
|
| 961 |
+
if not isinstance(original_scene, dict) or not isinstance(cf_scene, dict):
|
| 962 |
+
return score
|
| 963 |
+
|
| 964 |
+
orig_objs = original_scene.get('objects', []) or []
|
| 965 |
+
cf_objs = cf_scene.get('objects', []) or []
|
| 966 |
+
|
| 967 |
+
if len(orig_objs) != len(cf_objs):
|
| 968 |
+
score += 5.0
|
| 969 |
+
|
| 970 |
+
n = min(len(orig_objs), len(cf_objs))
|
| 971 |
+
for i in range(n):
|
| 972 |
+
o = orig_objs[i] or {}
|
| 973 |
+
c = cf_objs[i] or {}
|
| 974 |
+
for k in ['color', 'shape', 'size', 'material']:
|
| 975 |
+
if o.get(k) != c.get(k):
|
| 976 |
+
score += 1.0
|
| 977 |
+
|
| 978 |
+
o3 = o.get('3d_coords')
|
| 979 |
+
c3 = c.get('3d_coords')
|
| 980 |
+
try:
|
| 981 |
+
if o3 and c3 and len(o3) >= 2 and len(c3) >= 2:
|
| 982 |
+
dx = float(c3[0]) - float(o3[0])
|
| 983 |
+
dy = float(c3[1]) - float(o3[1])
|
| 984 |
+
dist = math.sqrt(dx * dx + dy * dy)
|
| 985 |
+
score += min(dist, 3.0)
|
| 986 |
+
except Exception:
|
| 987 |
+
pass
|
| 988 |
+
|
| 989 |
+
try:
|
| 990 |
+
if o.get('rotation') is not None and c.get('rotation') is not None:
|
| 991 |
+
rot_diff = abs(float(c.get('rotation')) - float(o.get('rotation')))
|
| 992 |
+
rot_diff = min(rot_diff, 360.0 - rot_diff)
|
| 993 |
+
score += min(rot_diff / 180.0, 1.0) * 0.5
|
| 994 |
+
except Exception:
|
| 995 |
+
pass
|
| 996 |
+
|
| 997 |
+
if 'background_color' in cf_scene and original_scene.get('background_color') != cf_scene.get('background_color'):
|
| 998 |
+
score += 1.0
|
| 999 |
+
if cf_scene.get('background_color') in ['white', 'dark_gray']:
|
| 1000 |
+
score += 0.5
|
| 1001 |
+
if 'lighting' in cf_scene and original_scene.get('lighting') != cf_scene.get('lighting'):
|
| 1002 |
+
score += 1.0
|
| 1003 |
+
if 'noise_level' in cf_scene and original_scene.get('noise_level') != cf_scene.get('noise_level'):
|
| 1004 |
+
score += {'light': 0.5, 'medium': 1.0, 'heavy': 1.5}.get(cf_scene.get('noise_level'), 0.8)
|
| 1005 |
+
if 'filter_type' in cf_scene and original_scene.get('filter_type') != cf_scene.get('filter_type'):
|
| 1006 |
+
filter_strength = cf_scene.get('filter_strength', 1.0)
|
| 1007 |
+
score += min(filter_strength, 2.0)
|
| 1008 |
+
|
| 1009 |
+
return score
|
| 1010 |
+
|
| 1011 |
+
def generate_one_with_min_change(cf_func, min_change_score=0.0, max_attempts=10):
|
| 1012 |
+
best_scene = None
|
| 1013 |
+
best_desc = None
|
| 1014 |
+
best_score = -1.0
|
| 1015 |
+
attempts = 0
|
| 1016 |
+
for attempt in range(max_attempts):
|
| 1017 |
+
attempts = attempt + 1
|
| 1018 |
+
if cf_func == cf_add_noise:
|
| 1019 |
+
candidate_scene, candidate_desc = cf_func(scene, min_noise_level=min_noise_level)
|
| 1020 |
+
else:
|
| 1021 |
+
candidate_scene, candidate_desc = cf_func(scene)
|
| 1022 |
+
score = compute_change_score(scene, candidate_scene)
|
| 1023 |
+
if score > best_score:
|
| 1024 |
+
best_scene, best_desc, best_score = candidate_scene, candidate_desc, score
|
| 1025 |
+
if score >= min_change_score:
|
| 1026 |
+
break
|
| 1027 |
+
return best_scene, best_desc, best_score, attempts
|
| 1028 |
+
|
| 1029 |
+
selected_types = None
|
| 1030 |
+
if semantic_only and negative_only:
|
| 1031 |
+
semantic_only = False
|
| 1032 |
+
negative_only = False
|
| 1033 |
+
if same_cf_type and cf_types:
|
| 1034 |
+
pool = cf_types
|
| 1035 |
+
if semantic_only:
|
| 1036 |
+
pool = [t for t in cf_types if t in IMAGE_COUNTERFACTUALS]
|
| 1037 |
+
elif negative_only:
|
| 1038 |
+
pool = [t for t in cf_types if t in NEGATIVE_COUNTERFACTUALS]
|
| 1039 |
+
if pool:
|
| 1040 |
+
selected_types = [pool[0]] * num_counterfactuals
|
| 1041 |
+
else:
|
| 1042 |
+
selected_types = [cf_types[0]] * num_counterfactuals if cf_types else None
|
| 1043 |
+
elif same_cf_type and not cf_types:
|
| 1044 |
+
if semantic_only:
|
| 1045 |
+
one_type = random.choice(list(IMAGE_COUNTERFACTUALS.keys()))
|
| 1046 |
+
elif negative_only:
|
| 1047 |
+
one_type = random.choice(DEFAULT_NEGATIVE_CF_TYPES)
|
| 1048 |
+
else:
|
| 1049 |
+
one_type = random.choice(list(COUNTERFACTUAL_TYPES.keys()))
|
| 1050 |
+
selected_types = [one_type] * num_counterfactuals
|
| 1051 |
+
elif cf_types:
|
| 1052 |
+
pool = cf_types
|
| 1053 |
+
if semantic_only:
|
| 1054 |
+
pool = [t for t in cf_types if t in IMAGE_COUNTERFACTUALS] or cf_types
|
| 1055 |
+
elif negative_only:
|
| 1056 |
+
pool = [t for t in cf_types if t in NEGATIVE_COUNTERFACTUALS] or cf_types
|
| 1057 |
+
# No duplicate CF types per scene: sample without replacement
|
| 1058 |
+
n = min(num_counterfactuals, len(pool))
|
| 1059 |
+
selected_types = random.sample(pool, n) if n > 0 else []
|
| 1060 |
+
elif semantic_only:
|
| 1061 |
+
pool = list(IMAGE_COUNTERFACTUALS.keys())
|
| 1062 |
+
selected_types = [random.choice(pool) for _ in range(num_counterfactuals)]
|
| 1063 |
+
elif negative_only:
|
| 1064 |
+
pool = DEFAULT_NEGATIVE_CF_TYPES
|
| 1065 |
+
selected_types = [random.choice(pool) for _ in range(num_counterfactuals)]
|
| 1066 |
+
|
| 1067 |
+
if selected_types is not None:
|
| 1068 |
+
for cf_type in selected_types:
|
| 1069 |
+
if cf_type not in COUNTERFACTUAL_TYPES:
|
| 1070 |
+
print(f"WARNING: Unknown CF type '{cf_type}', skipping")
|
| 1071 |
+
continue
|
| 1072 |
+
|
| 1073 |
+
cf_func = COUNTERFACTUAL_TYPES[cf_type]
|
| 1074 |
+
cf_scene, cf_desc, change_score, change_attempts = generate_one_with_min_change(
|
| 1075 |
+
cf_func,
|
| 1076 |
+
min_change_score=min_change_score,
|
| 1077 |
+
max_attempts=max_cf_attempts
|
| 1078 |
+
)
|
| 1079 |
+
cf_category = 'image_cf' if cf_type in IMAGE_COUNTERFACTUALS else 'negative_cf'
|
| 1080 |
+
|
| 1081 |
+
counterfactuals.append({
|
| 1082 |
+
'scene': cf_scene,
|
| 1083 |
+
'description': cf_desc,
|
| 1084 |
+
'type': cf_type,
|
| 1085 |
+
'cf_category': cf_category,
|
| 1086 |
+
'change_score': change_score,
|
| 1087 |
+
'change_attempts': change_attempts,
|
| 1088 |
+
})
|
| 1089 |
+
|
| 1090 |
+
elif num_counterfactuals >= 2:
|
| 1091 |
+
if semantic_only:
|
| 1092 |
+
pool = list(IMAGE_COUNTERFACTUALS.keys())
|
| 1093 |
+
for _ in range(num_counterfactuals):
|
| 1094 |
+
cf_type = random.choice(pool)
|
| 1095 |
+
cf_func = IMAGE_COUNTERFACTUALS[cf_type]
|
| 1096 |
+
cf_scene, cf_desc, change_score, change_attempts = generate_one_with_min_change(
|
| 1097 |
+
cf_func,
|
| 1098 |
+
min_change_score=min_change_score,
|
| 1099 |
+
max_attempts=max_cf_attempts
|
| 1100 |
+
)
|
| 1101 |
+
counterfactuals.append({
|
| 1102 |
+
'scene': cf_scene,
|
| 1103 |
+
'description': cf_desc,
|
| 1104 |
+
'type': cf_type,
|
| 1105 |
+
'cf_category': 'image_cf',
|
| 1106 |
+
'change_score': change_score,
|
| 1107 |
+
'change_attempts': change_attempts,
|
| 1108 |
+
})
|
| 1109 |
+
elif negative_only:
|
| 1110 |
+
pool = DEFAULT_NEGATIVE_CF_TYPES
|
| 1111 |
+
for _ in range(num_counterfactuals):
|
| 1112 |
+
cf_type = random.choice(pool)
|
| 1113 |
+
cf_func = NEGATIVE_COUNTERFACTUALS[cf_type]
|
| 1114 |
+
cf_scene, cf_desc, change_score, change_attempts = generate_one_with_min_change(
|
| 1115 |
+
cf_func,
|
| 1116 |
+
min_change_score=min_change_score,
|
| 1117 |
+
max_attempts=max_cf_attempts
|
| 1118 |
+
)
|
| 1119 |
+
counterfactuals.append({
|
| 1120 |
+
'scene': cf_scene,
|
| 1121 |
+
'description': cf_desc,
|
| 1122 |
+
'type': cf_type,
|
| 1123 |
+
'cf_category': 'negative_cf',
|
| 1124 |
+
'change_score': change_score,
|
| 1125 |
+
'change_attempts': change_attempts,
|
| 1126 |
+
})
|
| 1127 |
+
else:
|
| 1128 |
+
image_cf_type = random.choice(list(IMAGE_COUNTERFACTUALS.keys()))
|
| 1129 |
+
image_cf_func = IMAGE_COUNTERFACTUALS[image_cf_type]
|
| 1130 |
+
cf_scene, cf_desc, change_score, change_attempts = generate_one_with_min_change(
|
| 1131 |
+
image_cf_func,
|
| 1132 |
+
min_change_score=min_change_score,
|
| 1133 |
+
max_attempts=max_cf_attempts
|
| 1134 |
+
)
|
| 1135 |
+
counterfactuals.append({
|
| 1136 |
+
'scene': cf_scene,
|
| 1137 |
+
'description': cf_desc,
|
| 1138 |
+
'type': image_cf_type,
|
| 1139 |
+
'cf_category': 'image_cf',
|
| 1140 |
+
'change_score': change_score,
|
| 1141 |
+
'change_attempts': change_attempts,
|
| 1142 |
+
})
|
| 1143 |
+
|
| 1144 |
+
neg_cf_type = random.choice(DEFAULT_NEGATIVE_CF_TYPES)
|
| 1145 |
+
neg_cf_func = NEGATIVE_COUNTERFACTUALS[neg_cf_type]
|
| 1146 |
+
cf_scene, cf_desc, change_score, change_attempts = generate_one_with_min_change(
|
| 1147 |
+
neg_cf_func,
|
| 1148 |
+
min_change_score=min_change_score,
|
| 1149 |
+
max_attempts=max_cf_attempts
|
| 1150 |
+
)
|
| 1151 |
+
counterfactuals.append({
|
| 1152 |
+
'scene': cf_scene,
|
| 1153 |
+
'description': cf_desc,
|
| 1154 |
+
'type': neg_cf_type,
|
| 1155 |
+
'cf_category': 'negative_cf',
|
| 1156 |
+
'change_score': change_score,
|
| 1157 |
+
'change_attempts': change_attempts,
|
| 1158 |
+
})
|
| 1159 |
+
|
| 1160 |
+
remaining = num_counterfactuals - 2
|
| 1161 |
+
if remaining > 0:
|
| 1162 |
+
all_cf_types = list(COUNTERFACTUAL_TYPES.keys())
|
| 1163 |
+
used_types = {image_cf_type, neg_cf_type}
|
| 1164 |
+
available = [t for t in all_cf_types if t not in used_types and t != 'apply_fisheye']
|
| 1165 |
+
|
| 1166 |
+
extra_types = random.sample(available, min(remaining, len(available)))
|
| 1167 |
+
for cf_type in extra_types:
|
| 1168 |
+
cf_func = COUNTERFACTUAL_TYPES[cf_type]
|
| 1169 |
+
cf_scene, cf_desc, change_score, change_attempts = generate_one_with_min_change(
|
| 1170 |
+
cf_func,
|
| 1171 |
+
min_change_score=min_change_score,
|
| 1172 |
+
max_attempts=max_cf_attempts
|
| 1173 |
+
)
|
| 1174 |
+
cf_category = 'image_cf' if cf_type in IMAGE_COUNTERFACTUALS else 'negative_cf'
|
| 1175 |
+
counterfactuals.append({
|
| 1176 |
+
'scene': cf_scene,
|
| 1177 |
+
'description': cf_desc,
|
| 1178 |
+
'type': cf_type,
|
| 1179 |
+
'cf_category': cf_category,
|
| 1180 |
+
'change_score': change_score,
|
| 1181 |
+
'change_attempts': change_attempts,
|
| 1182 |
+
})
|
| 1183 |
+
|
| 1184 |
+
elif num_counterfactuals == 1:
|
| 1185 |
+
if semantic_only:
|
| 1186 |
+
cf_type = random.choice(list(IMAGE_COUNTERFACTUALS.keys()))
|
| 1187 |
+
cf_func = IMAGE_COUNTERFACTUALS[cf_type]
|
| 1188 |
+
cf_category = 'image_cf'
|
| 1189 |
+
elif negative_only:
|
| 1190 |
+
cf_type = random.choice(DEFAULT_NEGATIVE_CF_TYPES)
|
| 1191 |
+
cf_func = NEGATIVE_COUNTERFACTUALS[cf_type]
|
| 1192 |
+
cf_category = 'negative_cf'
|
| 1193 |
+
elif random.random() < 0.5:
|
| 1194 |
+
cf_type = random.choice(list(IMAGE_COUNTERFACTUALS.keys()))
|
| 1195 |
+
cf_func = IMAGE_COUNTERFACTUALS[cf_type]
|
| 1196 |
+
cf_category = 'image_cf'
|
| 1197 |
+
else:
|
| 1198 |
+
cf_type = random.choice(DEFAULT_NEGATIVE_CF_TYPES)
|
| 1199 |
+
cf_func = NEGATIVE_COUNTERFACTUALS[cf_type]
|
| 1200 |
+
cf_category = 'negative_cf'
|
| 1201 |
+
|
| 1202 |
+
cf_scene, cf_desc, change_score, change_attempts = generate_one_with_min_change(
|
| 1203 |
+
cf_func,
|
| 1204 |
+
min_change_score=min_change_score,
|
| 1205 |
+
max_attempts=max_cf_attempts
|
| 1206 |
+
)
|
| 1207 |
+
counterfactuals.append({
|
| 1208 |
+
'scene': cf_scene,
|
| 1209 |
+
'description': cf_desc,
|
| 1210 |
+
'type': cf_type,
|
| 1211 |
+
'cf_category': cf_category,
|
| 1212 |
+
'change_score': change_score,
|
| 1213 |
+
'change_attempts': change_attempts,
|
| 1214 |
+
})
|
| 1215 |
+
|
| 1216 |
+
return counterfactuals
|
| 1217 |
+
|
| 1218 |
+
def render_scene(blender_path, scene_file, output_image, use_gpu=0, samples=512, width=320, height=240):
|
| 1219 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 1220 |
+
render_script = os.path.join(current_dir, 'scripts', 'render.py')
|
| 1221 |
+
render_script = os.path.normpath(render_script)
|
| 1222 |
+
|
| 1223 |
+
base_scene = os.path.join(current_dir, 'data', 'base_scene.blend')
|
| 1224 |
+
properties_json = os.path.join(current_dir, 'data', 'properties.json')
|
| 1225 |
+
shape_dir = os.path.join(current_dir, 'data', 'shapes')
|
| 1226 |
+
material_dir = os.path.join(current_dir, 'data', 'materials')
|
| 1227 |
+
|
| 1228 |
+
base_scene = os.path.normpath(base_scene)
|
| 1229 |
+
properties_json = os.path.normpath(properties_json)
|
| 1230 |
+
shape_dir = os.path.normpath(shape_dir)
|
| 1231 |
+
material_dir = os.path.normpath(material_dir)
|
| 1232 |
+
scene_file = os.path.normpath(scene_file)
|
| 1233 |
+
output_image = os.path.normpath(output_image)
|
| 1234 |
+
|
| 1235 |
+
if not os.path.exists(base_scene):
|
| 1236 |
+
print(f" ERROR: Base scene file not found: {base_scene}")
|
| 1237 |
+
return False
|
| 1238 |
+
if not os.path.exists(properties_json):
|
| 1239 |
+
print(f" ERROR: Properties JSON not found: {properties_json}")
|
| 1240 |
+
return False
|
| 1241 |
+
if not os.path.exists(scene_file):
|
| 1242 |
+
print(f" ERROR: Scene file not found: {scene_file}")
|
| 1243 |
+
return False
|
| 1244 |
+
|
| 1245 |
+
output_dir = os.path.dirname(output_image)
|
| 1246 |
+
if output_dir and not os.path.exists(output_dir):
|
| 1247 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 1248 |
+
|
| 1249 |
+
output_image_abs = os.path.abspath(output_image)
|
| 1250 |
+
|
| 1251 |
+
cmd = [
|
| 1252 |
+
blender_path, '--background', '-noaudio', '--python', render_script, '--',
|
| 1253 |
+
'--scene_file', scene_file,
|
| 1254 |
+
'--output_image', output_image_abs,
|
| 1255 |
+
'--base_scene_blendfile', base_scene,
|
| 1256 |
+
'--properties_json', properties_json,
|
| 1257 |
+
'--shape_dir', shape_dir,
|
| 1258 |
+
'--material_dir', material_dir,
|
| 1259 |
+
'--use_gpu', str(use_gpu),
|
| 1260 |
+
'--render_num_samples', str(samples),
|
| 1261 |
+
'--width', str(width),
|
| 1262 |
+
'--height', str(height)
|
| 1263 |
+
]
|
| 1264 |
+
|
| 1265 |
+
env = os.environ.copy()
|
| 1266 |
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600, env=env)
|
| 1267 |
+
|
| 1268 |
+
if result.returncode == 0 and os.path.exists(output_image_abs):
|
| 1269 |
+
try:
|
| 1270 |
+
import json
|
| 1271 |
+
with open(scene_file, 'r') as f:
|
| 1272 |
+
scene_data = json.load(f)
|
| 1273 |
+
|
| 1274 |
+
filter_type = scene_data.get('filter_type')
|
| 1275 |
+
noise_level = scene_data.get('noise_level')
|
| 1276 |
+
|
| 1277 |
+
if filter_type or noise_level:
|
| 1278 |
+
try:
|
| 1279 |
+
from PIL import Image, ImageFilter
|
| 1280 |
+
import math
|
| 1281 |
+
|
| 1282 |
+
filter_strength = scene_data.get('filter_strength', 1.0)
|
| 1283 |
+
img = Image.open(output_image_abs)
|
| 1284 |
+
|
| 1285 |
+
if filter_type == 'blur':
|
| 1286 |
+
radius = max(1, int(filter_strength))
|
| 1287 |
+
img = img.filter(ImageFilter.GaussianBlur(radius=radius))
|
| 1288 |
+
|
| 1289 |
+
elif filter_type == 'vignette':
|
| 1290 |
+
width, height = img.size
|
| 1291 |
+
center_x, center_y = width // 2, height // 2
|
| 1292 |
+
max_dist = math.sqrt(center_x**2 + center_y**2)
|
| 1293 |
+
img = img.convert('RGB')
|
| 1294 |
+
pixels = img.load()
|
| 1295 |
+
for y in range(height):
|
| 1296 |
+
for x in range(width):
|
| 1297 |
+
dist = math.sqrt((x - center_x)**2 + (y - center_y)**2)
|
| 1298 |
+
factor = 1.0 - (dist / max_dist) * (filter_strength / 5.0)
|
| 1299 |
+
factor = max(0.0, min(1.0, factor))
|
| 1300 |
+
r, g, b = pixels[x, y]
|
| 1301 |
+
pixels[x, y] = (int(r * factor), int(g * factor), int(b * factor))
|
| 1302 |
+
|
| 1303 |
+
elif filter_type == 'fisheye':
|
| 1304 |
+
width, height = img.size
|
| 1305 |
+
center_x, center_y = width / 2.0, height / 2.0
|
| 1306 |
+
max_radius = min(center_x, center_y)
|
| 1307 |
+
img = img.convert('RGB')
|
| 1308 |
+
output = Image.new('RGB', (width, height))
|
| 1309 |
+
out_pixels = output.load()
|
| 1310 |
+
in_pixels = img.load()
|
| 1311 |
+
|
| 1312 |
+
distortion_strength = 0.55 * filter_strength
|
| 1313 |
+
|
| 1314 |
+
for y in range(height):
|
| 1315 |
+
for x in range(width):
|
| 1316 |
+
dx = (x - center_x) / max_radius
|
| 1317 |
+
dy = (y - center_y) / max_radius
|
| 1318 |
+
distance = math.sqrt(dx*dx + dy*dy)
|
| 1319 |
+
|
| 1320 |
+
if distance >= 1.0:
|
| 1321 |
+
out_pixels[x, y] = in_pixels[x, y]
|
| 1322 |
+
else:
|
| 1323 |
+
theta = math.atan2(dy, dx)
|
| 1324 |
+
r_normalized = distance
|
| 1325 |
+
|
| 1326 |
+
r_distorted = math.atan(2 * r_normalized * distortion_strength) / math.atan(2 * distortion_strength)
|
| 1327 |
+
r_distorted = min(0.999, r_distorted)
|
| 1328 |
+
|
| 1329 |
+
src_x = int(center_x + r_distorted * max_radius * math.cos(theta))
|
| 1330 |
+
src_y = int(center_y + r_distorted * max_radius * math.sin(theta))
|
| 1331 |
+
|
| 1332 |
+
if 0 <= src_x < width and 0 <= src_y < height:
|
| 1333 |
+
out_pixels[x, y] = in_pixels[src_x, src_y]
|
| 1334 |
+
else:
|
| 1335 |
+
out_pixels[x, y] = in_pixels[x, y]
|
| 1336 |
+
|
| 1337 |
+
img = output
|
| 1338 |
+
|
| 1339 |
+
elif filter_type == 'chromatic_aberration':
|
| 1340 |
+
width, height = img.size
|
| 1341 |
+
img = img.convert('RGB')
|
| 1342 |
+
output = Image.new('RGB', (width, height))
|
| 1343 |
+
out_pixels = output.load()
|
| 1344 |
+
in_pixels = img.load()
|
| 1345 |
+
|
| 1346 |
+
offset = int(filter_strength * 2)
|
| 1347 |
+
|
| 1348 |
+
for y in range(height):
|
| 1349 |
+
for x in range(width):
|
| 1350 |
+
r_x = max(0, min(width - 1, x - offset))
|
| 1351 |
+
g_x = x
|
| 1352 |
+
b_x = max(0, min(width - 1, x + offset))
|
| 1353 |
+
|
| 1354 |
+
r = in_pixels[r_x, y][0] if 0 <= r_x < width else 0
|
| 1355 |
+
g = in_pixels[g_x, y][1] if 0 <= g_x < width else 0
|
| 1356 |
+
b = in_pixels[b_x, y][2] if 0 <= b_x < width else 0
|
| 1357 |
+
|
| 1358 |
+
out_pixels[x, y] = (r, g, b)
|
| 1359 |
+
|
| 1360 |
+
img = output
|
| 1361 |
+
|
| 1362 |
+
if noise_level:
|
| 1363 |
+
import random
|
| 1364 |
+
noise_amounts = {'light': 10, 'medium': 25, 'heavy': 50}
|
| 1365 |
+
noise_amount = noise_amounts.get(noise_level, 20)
|
| 1366 |
+
img = img.convert('RGB')
|
| 1367 |
+
pixels = img.load()
|
| 1368 |
+
width, height = img.size
|
| 1369 |
+
|
| 1370 |
+
for y in range(height):
|
| 1371 |
+
for x in range(width):
|
| 1372 |
+
r, g, b = pixels[x, y]
|
| 1373 |
+
noise_r = random.randint(-noise_amount, noise_amount)
|
| 1374 |
+
noise_g = random.randint(-noise_amount, noise_amount)
|
| 1375 |
+
noise_b = random.randint(-noise_amount, noise_amount)
|
| 1376 |
+
|
| 1377 |
+
r = max(0, min(255, r + noise_r))
|
| 1378 |
+
g = max(0, min(255, g + noise_g))
|
| 1379 |
+
b = max(0, min(255, b + noise_b))
|
| 1380 |
+
|
| 1381 |
+
pixels[x, y] = (r, g, b)
|
| 1382 |
+
|
| 1383 |
+
img.save(output_image_abs)
|
| 1384 |
+
except ImportError:
|
| 1385 |
+
pass
|
| 1386 |
+
except Exception as e:
|
| 1387 |
+
print(f" Warning: Could not apply filter {filter_type}: {e}")
|
| 1388 |
+
|
| 1389 |
+
except Exception as e:
|
| 1390 |
+
pass
|
| 1391 |
+
|
| 1392 |
+
if result.returncode == 0 and os.path.exists(output_image_abs):
|
| 1393 |
+
return True
|
| 1394 |
+
else:
|
| 1395 |
+
print(f" ERROR rendering {output_image_abs}")
|
| 1396 |
+
print(f" Return code: {result.returncode}")
|
| 1397 |
+
print(f" Output file exists: {os.path.exists(output_image_abs)}")
|
| 1398 |
+
if result.stderr:
|
| 1399 |
+
print(f" Blender stderr (last 1000 chars):")
|
| 1400 |
+
print(result.stderr[-1000:])
|
| 1401 |
+
if result.stdout:
|
| 1402 |
+
print(f" Blender stdout (last 1000 chars):")
|
| 1403 |
+
print(result.stdout[-1000:])
|
| 1404 |
+
error_lines = [line for line in result.stdout.split('\n') if 'ERROR' in line.upper() or 'Traceback' in line or 'Exception' in line or 'failed' in line.lower()]
|
| 1405 |
+
if error_lines:
|
| 1406 |
+
error_msg = '\n'.join(error_lines[-20:])
|
| 1407 |
+
print(f" Error lines found: {error_msg}")
|
| 1408 |
+
return False
|
| 1409 |
+
|
| 1410 |
+
def save_scene(scene, output_path):
|
| 1411 |
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 1412 |
+
with open(output_path, 'w') as f:
|
| 1413 |
+
json.dump(scene, f, indent=2)
|
| 1414 |
+
|
| 1415 |
+
def list_counterfactual_types():
|
| 1416 |
+
print("\n" + "="*70)
|
| 1417 |
+
print("AVAILABLE COUNTERFACTUAL TYPES")
|
| 1418 |
+
print("="*70)
|
| 1419 |
+
|
| 1420 |
+
print("\nIMAGE COUNTERFACTUALS (Should change VQA answers):")
|
| 1421 |
+
print(" change_color - Change color of an object")
|
| 1422 |
+
print(" change_shape - Change shape of an object (cube/sphere/cylinder)")
|
| 1423 |
+
print(" change_size - Change size of an object (small/large)")
|
| 1424 |
+
print(" change_material - Change material of an object (metal/rubber)")
|
| 1425 |
+
print(" change_position - Move an object to a different location")
|
| 1426 |
+
print(" add_object - Add a new random object")
|
| 1427 |
+
print(" swap_attribute - Swap colors between two objects")
|
| 1428 |
+
print(" relational_flip - Move object from left of X to right of X")
|
| 1429 |
+
print(" remove_object - Remove a random object")
|
| 1430 |
+
print(" replace_object - Replace an object with a different one")
|
| 1431 |
+
|
| 1432 |
+
print("\nNEGATIVE COUNTERFACTUALS (Should NOT change VQA answers):")
|
| 1433 |
+
print(" change_background - Change background/ground color")
|
| 1434 |
+
print(" change_lighting - Change lighting conditions")
|
| 1435 |
+
print(" add_noise - Add image noise/grain")
|
| 1436 |
+
print(" occlusion_change - Move object to partially hide another")
|
| 1437 |
+
print(" apply_fisheye - Apply fisheye lens distortion")
|
| 1438 |
+
print(" apply_blur - Apply Gaussian blur")
|
| 1439 |
+
print(" apply_vignette - Apply vignette (edge darkening)")
|
| 1440 |
+
print(" apply_chromatic_aberration - Apply chromatic aberration (color fringing)")
|
| 1441 |
+
|
| 1442 |
+
print("\n" + "="*70)
|
| 1443 |
+
print("\nUsage examples:")
|
| 1444 |
+
print(" # Use specific types")
|
| 1445 |
+
print(" python pipeline.py --num_scenes 10 --cf_types change_color change_position")
|
| 1446 |
+
print(" ")
|
| 1447 |
+
print(" # Mix image and negative CFs")
|
| 1448 |
+
print(" python pipeline.py --num_scenes 10 --cf_types change_shape change_lighting")
|
| 1449 |
+
print(" ")
|
| 1450 |
+
print(" # Only negative CFs")
|
| 1451 |
+
print(" python pipeline.py --num_scenes 10 --cf_types change_background add_noise")
|
| 1452 |
+
print("="*70 + "\n")
|
| 1453 |
+
|
| 1454 |
+
def find_scene_files(scenes_dir):
|
| 1455 |
+
scene_files = glob.glob(os.path.join(scenes_dir, 'scene_*.json'))
|
| 1456 |
+
scene_sets = {}
|
| 1457 |
+
|
| 1458 |
+
for scene_file in scene_files:
|
| 1459 |
+
basename = os.path.basename(scene_file)
|
| 1460 |
+
parts = basename.replace('.json', '').split('_')
|
| 1461 |
+
if len(parts) >= 3:
|
| 1462 |
+
scene_num = int(parts[1])
|
| 1463 |
+
variant = '_'.join(parts[2:])
|
| 1464 |
+
|
| 1465 |
+
if scene_num not in scene_sets:
|
| 1466 |
+
scene_sets[scene_num] = {}
|
| 1467 |
+
scene_sets[scene_num][variant] = scene_file
|
| 1468 |
+
|
| 1469 |
+
return scene_sets
|
| 1470 |
+
|
| 1471 |
+
def render_existing_scenes(args):
|
| 1472 |
+
import json
|
| 1473 |
+
|
| 1474 |
+
if args.run_dir:
|
| 1475 |
+
run_dir = args.run_dir
|
| 1476 |
+
elif args.run_name:
|
| 1477 |
+
run_dir = os.path.join(args.output_dir, args.run_name)
|
| 1478 |
+
elif args.auto_latest:
|
| 1479 |
+
if not os.path.exists(args.output_dir):
|
| 1480 |
+
print(f"ERROR: Output directory does not exist: {args.output_dir}")
|
| 1481 |
+
return
|
| 1482 |
+
|
| 1483 |
+
subdirs = [d for d in os.listdir(args.output_dir)
|
| 1484 |
+
if os.path.isdir(os.path.join(args.output_dir, d))]
|
| 1485 |
+
if not subdirs:
|
| 1486 |
+
print(f"ERROR: No run directories found in {args.output_dir}")
|
| 1487 |
+
return
|
| 1488 |
+
|
| 1489 |
+
dirs_with_time = [(d, os.path.getmtime(os.path.join(args.output_dir, d)))
|
| 1490 |
+
for d in subdirs]
|
| 1491 |
+
latest = max(dirs_with_time, key=lambda x: x[1])[0]
|
| 1492 |
+
run_dir = os.path.join(args.output_dir, latest)
|
| 1493 |
+
print(f"Using latest run: {run_dir}")
|
| 1494 |
+
else:
|
| 1495 |
+
print("ERROR: Must specify --run_dir, --run_name, or --auto_latest for --render_only")
|
| 1496 |
+
return
|
| 1497 |
+
|
| 1498 |
+
if not os.path.exists(run_dir):
|
| 1499 |
+
print(f"ERROR: Run directory does not exist: {run_dir}")
|
| 1500 |
+
return
|
| 1501 |
+
|
| 1502 |
+
scenes_dir = os.path.join(run_dir, 'scenes')
|
| 1503 |
+
images_dir = os.path.join(run_dir, 'images')
|
| 1504 |
+
|
| 1505 |
+
if not os.path.exists(scenes_dir):
|
| 1506 |
+
print(f"ERROR: Scenes directory does not exist: {scenes_dir}")
|
| 1507 |
+
return
|
| 1508 |
+
|
| 1509 |
+
os.makedirs(images_dir, exist_ok=True)
|
| 1510 |
+
|
| 1511 |
+
blender_path = args.blender_path or find_blender()
|
| 1512 |
+
print(f"Using Blender: {blender_path}")
|
| 1513 |
+
|
| 1514 |
+
print("\nPreparing render scripts...")
|
| 1515 |
+
create_patched_render_script()
|
| 1516 |
+
|
| 1517 |
+
scene_sets = find_scene_files(scenes_dir)
|
| 1518 |
+
total_scenes = len(scene_sets)
|
| 1519 |
+
|
| 1520 |
+
if total_scenes == 0:
|
| 1521 |
+
print(f"ERROR: No scene JSON files found in {scenes_dir}")
|
| 1522 |
+
return
|
| 1523 |
+
|
| 1524 |
+
print(f"\nFound {total_scenes} scene sets to render")
|
| 1525 |
+
|
| 1526 |
+
checkpoint_file = os.path.join(run_dir, 'render_checkpoint.json')
|
| 1527 |
+
|
| 1528 |
+
completed_scenes = set()
|
| 1529 |
+
if args.resume:
|
| 1530 |
+
completed_scenes = load_checkpoint(checkpoint_file)
|
| 1531 |
+
rendered_scenes = get_completed_scenes_from_folder(images_dir)
|
| 1532 |
+
completed_scenes.update(rendered_scenes)
|
| 1533 |
+
|
| 1534 |
+
if completed_scenes:
|
| 1535 |
+
print(f"\n[RESUME] Found {len(completed_scenes)} already rendered scenes")
|
| 1536 |
+
|
| 1537 |
+
print("\n" + "="*70)
|
| 1538 |
+
print(f"RENDERING {total_scenes} SCENE SETS")
|
| 1539 |
+
print("="*70)
|
| 1540 |
+
|
| 1541 |
+
successful_renders = 0
|
| 1542 |
+
|
| 1543 |
+
for scene_num in sorted(scene_sets.keys()):
|
| 1544 |
+
if scene_num in completed_scenes:
|
| 1545 |
+
print(f"\n[SKIP] Skipping scene {scene_num} (already rendered)")
|
| 1546 |
+
successful_renders += 1
|
| 1547 |
+
continue
|
| 1548 |
+
|
| 1549 |
+
print(f"\n{'='*70}")
|
| 1550 |
+
print(f"SCENE SET {scene_num+1}/{total_scenes} (Scene #{scene_num})")
|
| 1551 |
+
print(f"{'='*70}")
|
| 1552 |
+
|
| 1553 |
+
scene_set = scene_sets[scene_num]
|
| 1554 |
+
total_to_render = len(scene_set)
|
| 1555 |
+
render_success = 0
|
| 1556 |
+
|
| 1557 |
+
for variant, scene_file in scene_set.items():
|
| 1558 |
+
scene_prefix = f"scene_{scene_num:04d}"
|
| 1559 |
+
image_file = os.path.join(images_dir, f"{scene_prefix}_{variant}.png")
|
| 1560 |
+
|
| 1561 |
+
print(f" Rendering {variant}...")
|
| 1562 |
+
if render_scene(blender_path, scene_file, image_file,
|
| 1563 |
+
args.use_gpu, args.samples, args.width, args.height):
|
| 1564 |
+
render_success += 1
|
| 1565 |
+
print(f" [OK] {variant}")
|
| 1566 |
+
else:
|
| 1567 |
+
print(f" [FAILED] {variant}")
|
| 1568 |
+
|
| 1569 |
+
if render_success == total_to_render:
|
| 1570 |
+
successful_renders += 1
|
| 1571 |
+
completed_scenes.add(scene_num)
|
| 1572 |
+
save_checkpoint(checkpoint_file, list(completed_scenes))
|
| 1573 |
+
|
| 1574 |
+
print(f" [OK] Rendered {render_success}/{total_to_render} images")
|
| 1575 |
+
|
| 1576 |
+
metadata_path = os.path.join(run_dir, 'run_metadata.json')
|
| 1577 |
+
if os.path.exists(metadata_path):
|
| 1578 |
+
with open(metadata_path, 'r') as f:
|
| 1579 |
+
metadata = json.load(f)
|
| 1580 |
+
metadata['successful_renders'] = successful_renders
|
| 1581 |
+
metadata['rendered_timestamp'] = datetime.now().isoformat()
|
| 1582 |
+
with open(metadata_path, 'w') as f:
|
| 1583 |
+
json.dump(metadata, f, indent=2)
|
| 1584 |
+
|
| 1585 |
+
print("\n" + "="*70)
|
| 1586 |
+
print("RENDERING COMPLETE")
|
| 1587 |
+
print("="*70)
|
| 1588 |
+
print(f"Run directory: {run_dir}")
|
| 1589 |
+
print(f"Successfully rendered: {successful_renders}/{total_scenes} scene sets")
|
| 1590 |
+
print(f"\nOutput:")
|
| 1591 |
+
print(f" Images: {images_dir}/")
|
| 1592 |
+
print(f" Checkpoint: {checkpoint_file}")
|
| 1593 |
+
print("="*70)
|
| 1594 |
+
|
| 1595 |
+
def is_running_on_huggingface():
|
| 1596 |
+
return (
|
| 1597 |
+
os.environ.get('SPACES_REPO_TYPE') is not None or
|
| 1598 |
+
os.environ.get('SPACES_REPO_ID') is not None or
|
| 1599 |
+
os.environ.get('SPACE_ID') is not None or
|
| 1600 |
+
os.environ.get('HF_HOME') is not None or
|
| 1601 |
+
'huggingface' in str(os.environ.get('PATH', '')).lower() or
|
| 1602 |
+
os.path.exists('/.dockerenv')
|
| 1603 |
+
)
|
| 1604 |
+
|
| 1605 |
+
def create_downloadable_archive(run_dir, output_zip_path=None):
|
| 1606 |
+
if not is_running_on_huggingface():
|
| 1607 |
+
return None
|
| 1608 |
+
|
| 1609 |
+
if not os.path.exists(run_dir):
|
| 1610 |
+
print(f"Warning: Run directory does not exist: {run_dir}")
|
| 1611 |
+
return None
|
| 1612 |
+
|
| 1613 |
+
if output_zip_path is None:
|
| 1614 |
+
run_name = os.path.basename(run_dir)
|
| 1615 |
+
output_zip_path = os.path.join(os.path.dirname(os.path.dirname(run_dir)) if 'output' in run_dir else '.', f"{run_name}.zip")
|
| 1616 |
+
output_zip_path = os.path.abspath(output_zip_path)
|
| 1617 |
+
|
| 1618 |
+
print(f"\n[ARCHIVE] Creating downloadable archive: {output_zip_path}")
|
| 1619 |
+
print(f" (This file will be available in the Files tab)")
|
| 1620 |
+
|
| 1621 |
+
try:
|
| 1622 |
+
with zipfile.ZipFile(output_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
| 1623 |
+
for root, dirs, files in os.walk(run_dir):
|
| 1624 |
+
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
| 1625 |
+
|
| 1626 |
+
for file in files:
|
| 1627 |
+
if file.startswith('.'):
|
| 1628 |
+
continue
|
| 1629 |
+
|
| 1630 |
+
file_path = os.path.join(root, file)
|
| 1631 |
+
arcname = os.path.relpath(file_path, os.path.dirname(run_dir))
|
| 1632 |
+
zipf.write(file_path, arcname)
|
| 1633 |
+
|
| 1634 |
+
zip_size = os.path.getsize(output_zip_path) / (1024 * 1024)
|
| 1635 |
+
print(f"[OK] Archive created successfully ({zip_size:.2f} MB)")
|
| 1636 |
+
print(f" File location: {os.path.abspath(output_zip_path)}")
|
| 1637 |
+
print(f" To download: Go to the 'Files' tab in your Hugging Face Space and download '{os.path.basename(output_zip_path)}'")
|
| 1638 |
+
return output_zip_path
|
| 1639 |
+
except Exception as e:
|
| 1640 |
+
print(f"[WARNING] Could not create archive: {e}")
|
| 1641 |
+
return None
|
| 1642 |
+
|
| 1643 |
+
def save_run_metadata(run_dir, args, successful_scenes, successful_renders):
|
| 1644 |
+
metadata = {
|
| 1645 |
+
'timestamp': datetime.now().isoformat(),
|
| 1646 |
+
'run_directory': run_dir,
|
| 1647 |
+
'arguments': {
|
| 1648 |
+
'num_scenes': args.num_scenes,
|
| 1649 |
+
'num_objects': args.num_objects,
|
| 1650 |
+
'min_objects': args.min_objects,
|
| 1651 |
+
'max_objects': args.max_objects,
|
| 1652 |
+
'num_counterfactuals': args.num_counterfactuals,
|
| 1653 |
+
'use_gpu': args.use_gpu,
|
| 1654 |
+
'samples': args.samples,
|
| 1655 |
+
'width': args.width,
|
| 1656 |
+
'height': args.height,
|
| 1657 |
+
'cf_types': args.cf_types if args.cf_types else 'default (1 image + 1 negative)',
|
| 1658 |
+
'semantic_only': getattr(args, 'semantic_only', False),
|
| 1659 |
+
'negative_only': getattr(args, 'negative_only', False),
|
| 1660 |
+
'min_cf_change_score': args.min_cf_change_score,
|
| 1661 |
+
'max_cf_attempts': args.max_cf_attempts,
|
| 1662 |
+
},
|
| 1663 |
+
|
| 1664 |
+
'results': {
|
| 1665 |
+
'scenes_generated': successful_scenes,
|
| 1666 |
+
'scenes_rendered': successful_renders,
|
| 1667 |
+
'total_images': successful_renders * (args.num_counterfactuals + 1) if not args.skip_render else 0,
|
| 1668 |
+
},
|
| 1669 |
+
'cf_types_used': args.cf_types if args.cf_types else list(COUNTERFACTUAL_TYPES.keys())
|
| 1670 |
+
}
|
| 1671 |
+
|
| 1672 |
+
metadata_path = os.path.join(run_dir, 'run_metadata.json')
|
| 1673 |
+
with open(metadata_path, 'w') as f:
|
| 1674 |
+
json.dump(metadata, f, indent=2)
|
| 1675 |
+
|
| 1676 |
+
print(f"\n[OK] Saved run metadata to: {metadata_path}")
|
| 1677 |
+
|
| 1678 |
+
|
| 1679 |
+
def regenerate_scene_sets(args):
|
| 1680 |
+
run_dir = os.path.join(args.output_dir, args.run_name)
|
| 1681 |
+
if not os.path.exists(run_dir):
|
| 1682 |
+
print(f"ERROR: Run directory does not exist: {run_dir}")
|
| 1683 |
+
return
|
| 1684 |
+
|
| 1685 |
+
metadata_path = os.path.join(run_dir, 'run_metadata.json')
|
| 1686 |
+
if not os.path.exists(metadata_path):
|
| 1687 |
+
print(f"ERROR: run_metadata.json not found in {run_dir}. Cannot determine original settings.")
|
| 1688 |
+
return
|
| 1689 |
+
|
| 1690 |
+
with open(metadata_path, 'r') as f:
|
| 1691 |
+
metadata = json.load(f)
|
| 1692 |
+
meta_args = metadata.get('arguments', {})
|
| 1693 |
+
|
| 1694 |
+
scenes_dir = os.path.join(run_dir, 'scenes')
|
| 1695 |
+
images_dir = os.path.join(run_dir, 'images')
|
| 1696 |
+
os.makedirs(scenes_dir, exist_ok=True)
|
| 1697 |
+
os.makedirs(images_dir, exist_ok=True)
|
| 1698 |
+
|
| 1699 |
+
blender_path = args.blender_path or find_blender()
|
| 1700 |
+
print(f"Using Blender: {blender_path}")
|
| 1701 |
+
print("\nPreparing scripts...")
|
| 1702 |
+
create_patched_render_script()
|
| 1703 |
+
|
| 1704 |
+
scene_indices = sorted(set(args.regenerate))
|
| 1705 |
+
num_counterfactuals = meta_args.get('num_counterfactuals', 2)
|
| 1706 |
+
cf_types = meta_args.get('cf_types')
|
| 1707 |
+
if isinstance(cf_types, list) and cf_types:
|
| 1708 |
+
pass
|
| 1709 |
+
else:
|
| 1710 |
+
cf_types = None
|
| 1711 |
+
|
| 1712 |
+
use_gpu = meta_args.get('use_gpu', 0)
|
| 1713 |
+
samples = meta_args.get('samples', 512)
|
| 1714 |
+
width = meta_args.get('width', 320)
|
| 1715 |
+
height = meta_args.get('height', 240)
|
| 1716 |
+
|
| 1717 |
+
print(f"\n{'='*70}")
|
| 1718 |
+
print(f"REGENERATING {len(scene_indices)} SCENE SETS: {scene_indices}")
|
| 1719 |
+
print(f"{'='*70}")
|
| 1720 |
+
|
| 1721 |
+
temp_run_id = os.path.basename(run_dir)
|
| 1722 |
+
checkpoint_file = os.path.join(run_dir, 'checkpoint.json')
|
| 1723 |
+
completed_scenes = load_checkpoint(checkpoint_file)
|
| 1724 |
+
|
| 1725 |
+
for i in scene_indices:
|
| 1726 |
+
print(f"\n{'='*70}")
|
| 1727 |
+
print(f"REGENERATING SCENE SET #{i}")
|
| 1728 |
+
print(f"{'='*70}")
|
| 1729 |
+
|
| 1730 |
+
num_objects = meta_args.get('num_objects')
|
| 1731 |
+
if num_objects is None:
|
| 1732 |
+
min_objs = meta_args.get('min_objects', 3)
|
| 1733 |
+
max_objs = meta_args.get('max_objects', 7)
|
| 1734 |
+
num_objects = random.randint(min_objs, max_objs)
|
| 1735 |
+
|
| 1736 |
+
base_scene = None
|
| 1737 |
+
for retry in range(3):
|
| 1738 |
+
base_scene = generate_base_scene(num_objects, blender_path, i, temp_run_dir=temp_run_id)
|
| 1739 |
+
if base_scene and len(base_scene.get('objects', [])) > 0:
|
| 1740 |
+
break
|
| 1741 |
+
print(f" Retry {retry + 1}/3...")
|
| 1742 |
+
|
| 1743 |
+
if not base_scene or len(base_scene.get('objects', [])) == 0:
|
| 1744 |
+
print(f" [FAILED] Could not generate base scene for #{i}")
|
| 1745 |
+
continue
|
| 1746 |
+
|
| 1747 |
+
min_cf_score = meta_args.get('min_cf_change_score', 1.0)
|
| 1748 |
+
max_cf_attempts = meta_args.get('max_cf_attempts', 10)
|
| 1749 |
+
min_noise = meta_args.get('min_noise_level', 'light')
|
| 1750 |
+
|
| 1751 |
+
counterfactuals = generate_counterfactuals(
|
| 1752 |
+
base_scene,
|
| 1753 |
+
num_counterfactuals,
|
| 1754 |
+
cf_types=cf_types,
|
| 1755 |
+
same_cf_type=meta_args.get('same_cf_type', False),
|
| 1756 |
+
min_change_score=min_cf_score,
|
| 1757 |
+
max_cf_attempts=max_cf_attempts,
|
| 1758 |
+
min_noise_level=min_noise,
|
| 1759 |
+
semantic_only=meta_args.get('semantic_only', False),
|
| 1760 |
+
negative_only=meta_args.get('negative_only', False)
|
| 1761 |
+
)
|
| 1762 |
+
|
| 1763 |
+
for idx, cf in enumerate(counterfactuals):
|
| 1764 |
+
print(f" CF{idx+1} [{cf.get('cf_category', '?')}] ({cf.get('type', '?')}): {cf.get('description', '')}")
|
| 1765 |
+
|
| 1766 |
+
scene_prefix = f"scene_{i:04d}"
|
| 1767 |
+
scene_paths = {'original': os.path.join(scenes_dir, f"{scene_prefix}_original.json")}
|
| 1768 |
+
image_paths = {'original': os.path.join(images_dir, f"{scene_prefix}_original.png")}
|
| 1769 |
+
|
| 1770 |
+
base_scene['cf_metadata'] = {
|
| 1771 |
+
'variant': 'original', 'is_counterfactual': False, 'cf_index': None,
|
| 1772 |
+
'cf_category': 'original', 'cf_type': None, 'cf_description': None, 'source_scene': scene_prefix,
|
| 1773 |
+
}
|
| 1774 |
+
save_scene(base_scene, scene_paths['original'])
|
| 1775 |
+
|
| 1776 |
+
for idx, cf in enumerate(counterfactuals):
|
| 1777 |
+
cf_name = f"cf{idx+1}"
|
| 1778 |
+
scene_paths[cf_name] = os.path.join(scenes_dir, f"{scene_prefix}_{cf_name}.json")
|
| 1779 |
+
image_paths[cf_name] = os.path.join(images_dir, f"{scene_prefix}_{cf_name}.png")
|
| 1780 |
+
cf_scene = cf['scene']
|
| 1781 |
+
cf_scene['cf_metadata'] = {
|
| 1782 |
+
'variant': cf_name, 'is_counterfactual': True, 'cf_index': idx + 1,
|
| 1783 |
+
'cf_category': cf.get('cf_category', 'unknown'), 'cf_type': cf.get('type', None),
|
| 1784 |
+
'cf_description': cf.get('description', None), 'change_score': cf.get('change_score'),
|
| 1785 |
+
'change_attempts': cf.get('change_attempts'), 'source_scene': scene_prefix,
|
| 1786 |
+
}
|
| 1787 |
+
save_scene(cf_scene, scene_paths[cf_name])
|
| 1788 |
+
|
| 1789 |
+
print(f" [OK] Saved {len(counterfactuals) + 1} scene files")
|
| 1790 |
+
|
| 1791 |
+
if not args.skip_render:
|
| 1792 |
+
print(" Rendering...")
|
| 1793 |
+
render_success = 0
|
| 1794 |
+
for scene_type, scene_path in scene_paths.items():
|
| 1795 |
+
if render_scene(blender_path, scene_path, image_paths[scene_type],
|
| 1796 |
+
use_gpu, samples, width, height):
|
| 1797 |
+
render_success += 1
|
| 1798 |
+
print(f" [OK] {scene_type}")
|
| 1799 |
+
print(f" [OK] Rendered {render_success}/{len(scene_paths)} images")
|
| 1800 |
+
completed_scenes.add(i)
|
| 1801 |
+
|
| 1802 |
+
save_checkpoint(checkpoint_file, list(completed_scenes))
|
| 1803 |
+
|
| 1804 |
+
temp_run_path = os.path.join(os.getcwd(), 'temp_output', temp_run_id)
|
| 1805 |
+
if os.path.exists(temp_run_path):
|
| 1806 |
+
shutil.rmtree(temp_run_path)
|
| 1807 |
+
if os.path.exists('render_images_patched.py'):
|
| 1808 |
+
try:
|
| 1809 |
+
os.remove('render_images_patched.py')
|
| 1810 |
+
except Exception:
|
| 1811 |
+
pass
|
| 1812 |
+
|
| 1813 |
+
print(f"\n{'='*70}")
|
| 1814 |
+
print("REGENERATION COMPLETE")
|
| 1815 |
+
print(f"{'='*70}")
|
| 1816 |
+
print(f"Regenerated {len(scene_indices)} scene sets: {scene_indices}")
|
| 1817 |
+
print(f"Run directory: {run_dir}")
|
| 1818 |
+
|
| 1819 |
+
if args.generate_questions:
|
| 1820 |
+
if generate_mapping_with_questions is None:
|
| 1821 |
+
print("\n[WARNING] Questions module not found. Skipping CSV generation.")
|
| 1822 |
+
else:
|
| 1823 |
+
print("\nRegenerating questions CSV...")
|
| 1824 |
+
try:
|
| 1825 |
+
generate_mapping_with_questions(
|
| 1826 |
+
run_dir, args.csv_name, generate_questions=True,
|
| 1827 |
+
strict_question_validation=not getattr(args, 'no_strict_validation', False)
|
| 1828 |
+
)
|
| 1829 |
+
print(f"[OK] CSV saved to: {os.path.join(run_dir, args.csv_name)}")
|
| 1830 |
+
except Exception as e:
|
| 1831 |
+
print(f"[ERROR] Questions: {e}")
|
| 1832 |
+
import traceback
|
| 1833 |
+
traceback.print_exc()
|
| 1834 |
+
|
| 1835 |
+
|
| 1836 |
+
def filter_same_answer_scenes(run_dir, csv_filename):
|
| 1837 |
+
"""Remove CSV rows where CF1 or CF2 answer matches original; delete those scenes' images and scene JSONs."""
|
| 1838 |
+
csv_path = os.path.join(run_dir, csv_filename)
|
| 1839 |
+
if not os.path.isfile(csv_path):
|
| 1840 |
+
return
|
| 1841 |
+
with open(csv_path, 'r', encoding='utf-8') as f:
|
| 1842 |
+
reader = csv.reader(f)
|
| 1843 |
+
header = next(reader)
|
| 1844 |
+
try:
|
| 1845 |
+
idx_orig_ans = header.index('original_image_answer_to_original_question')
|
| 1846 |
+
idx_cf1_ans = header.index('cf1_image_answer_to_cf1_question')
|
| 1847 |
+
idx_cf2_ans = header.index('cf2_image_answer_to_cf2_question')
|
| 1848 |
+
idx_orig_img = header.index('original_image')
|
| 1849 |
+
except ValueError:
|
| 1850 |
+
return
|
| 1851 |
+
kept_rows = [header]
|
| 1852 |
+
removed_scene_ids = set()
|
| 1853 |
+
with open(csv_path, 'r', encoding='utf-8') as f:
|
| 1854 |
+
reader = csv.reader(f)
|
| 1855 |
+
next(reader)
|
| 1856 |
+
for row in reader:
|
| 1857 |
+
if len(row) <= max(idx_orig_ans, idx_cf1_ans, idx_cf2_ans, idx_orig_img):
|
| 1858 |
+
kept_rows.append(row)
|
| 1859 |
+
continue
|
| 1860 |
+
o = str(row[idx_orig_ans]).strip().lower()
|
| 1861 |
+
c1 = str(row[idx_cf1_ans]).strip().lower()
|
| 1862 |
+
c2 = str(row[idx_cf2_ans]).strip().lower()
|
| 1863 |
+
if o == c1 or o == c2:
|
| 1864 |
+
orig_img = row[idx_orig_img]
|
| 1865 |
+
if orig_img.endswith('_original.png'):
|
| 1866 |
+
scene_id = orig_img.replace('_original.png', '')
|
| 1867 |
+
removed_scene_ids.add(scene_id)
|
| 1868 |
+
continue
|
| 1869 |
+
kept_rows.append(row)
|
| 1870 |
+
if not removed_scene_ids:
|
| 1871 |
+
return
|
| 1872 |
+
with open(csv_path, 'w', newline='', encoding='utf-8') as f:
|
| 1873 |
+
writer = csv.writer(f, quoting=csv.QUOTE_ALL)
|
| 1874 |
+
writer.writerows(kept_rows)
|
| 1875 |
+
images_dir = os.path.join(run_dir, 'images')
|
| 1876 |
+
scenes_dir = os.path.join(run_dir, 'scenes')
|
| 1877 |
+
deleted = 0
|
| 1878 |
+
for scene_id in removed_scene_ids:
|
| 1879 |
+
for suffix in ('_original', '_cf1', '_cf2'):
|
| 1880 |
+
for d, ext in [(images_dir, '.png'), (scenes_dir, '.json')]:
|
| 1881 |
+
if not os.path.isdir(d):
|
| 1882 |
+
continue
|
| 1883 |
+
fn = scene_id + suffix + ext
|
| 1884 |
+
fp = os.path.join(d, fn)
|
| 1885 |
+
if os.path.isfile(fp):
|
| 1886 |
+
try:
|
| 1887 |
+
os.remove(fp)
|
| 1888 |
+
deleted += 1
|
| 1889 |
+
except OSError:
|
| 1890 |
+
pass
|
| 1891 |
+
print(f"\n[OK] Filtered {len(removed_scene_ids)} scenes where answers matched; removed {deleted} files. CSV now has {len(kept_rows) - 1} rows.")
|
| 1892 |
+
|
| 1893 |
+
|
| 1894 |
+
def main():
|
| 1895 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 1896 |
+
os.chdir(script_dir)
|
| 1897 |
+
|
| 1898 |
+
parser = argparse.ArgumentParser(description='Generate and render multiple CLEVR scene sets with diverse counterfactuals and resume support')
|
| 1899 |
+
parser.add_argument('--num_scenes', type=int, default=5,
|
| 1900 |
+
help='Number of scene sets to generate')
|
| 1901 |
+
parser.add_argument('--num_objects', type=int, default=None,
|
| 1902 |
+
help='Fixed number of objects per scene')
|
| 1903 |
+
parser.add_argument('--min_objects', type=int, default=3,
|
| 1904 |
+
help='Minimum objects per scene (if num_objects not set)')
|
| 1905 |
+
parser.add_argument('--max_objects', type=int, default=7,
|
| 1906 |
+
help='Maximum objects per scene (if num_objects not set)')
|
| 1907 |
+
parser.add_argument('--num_counterfactuals', type=int, default=2,
|
| 1908 |
+
help='Number of counterfactual variants per scene (default: 2 = 1 Image CF + 1 Negative CF)')
|
| 1909 |
+
parser.add_argument('--blender_path', default=None,
|
| 1910 |
+
help='Path to Blender executable')
|
| 1911 |
+
parser.add_argument('--output_dir', default='output',
|
| 1912 |
+
help='Base output directory')
|
| 1913 |
+
parser.add_argument('--run_name', default=None,
|
| 1914 |
+
help='Optional name for this run (required for resume)')
|
| 1915 |
+
parser.add_argument('--resume', action='store_true',
|
| 1916 |
+
help='Resume from last checkpoint (requires --run_name)')
|
| 1917 |
+
parser.add_argument('--use_gpu', type=int, default=0,
|
| 1918 |
+
help='Use GPU rendering (0 or 1)')
|
| 1919 |
+
parser.add_argument('--samples', type=int, default=512,
|
| 1920 |
+
help='Number of render samples')
|
| 1921 |
+
parser.add_argument('--width', type=int, default=320,
|
| 1922 |
+
help='Image width in pixels (default: 320)')
|
| 1923 |
+
parser.add_argument('--height', type=int, default=240,
|
| 1924 |
+
help='Image height in pixels (default: 240)')
|
| 1925 |
+
parser.add_argument('--skip_render', action='store_true',
|
| 1926 |
+
help='Only generate scenes, skip rendering')
|
| 1927 |
+
parser.add_argument('--render_only', action='store_true',
|
| 1928 |
+
help='Only render existing scene JSON files. Requires --run_dir, --run_name, or --auto_latest')
|
| 1929 |
+
parser.add_argument('--run_dir', type=str, default=None,
|
| 1930 |
+
help='Run directory containing scenes/ folder (for --render_only mode)')
|
| 1931 |
+
parser.add_argument('--auto_latest', action='store_true',
|
| 1932 |
+
help='Automatically use the latest run in output_dir (for --render_only mode)')
|
| 1933 |
+
parser.add_argument('--cf_types', nargs='+',
|
| 1934 |
+
choices=[
|
| 1935 |
+
'change_color', 'change_shape', 'change_size',
|
| 1936 |
+
'change_material', 'change_position',
|
| 1937 |
+
'add_object', 'remove_object', 'replace_object',
|
| 1938 |
+
'swap_attribute', 'occlusion_change', 'relational_flip',
|
| 1939 |
+
'change_background',
|
| 1940 |
+
'change_lighting', 'add_noise',
|
| 1941 |
+
'apply_fisheye', 'apply_blur', 'apply_vignette', 'apply_chromatic_aberration'
|
| 1942 |
+
],
|
| 1943 |
+
help='Specific counterfactual types to use (if not specified, uses default: 1 image CF + 1 negative CF)')
|
| 1944 |
+
parser.add_argument('--semantic_only', action='store_true',
|
| 1945 |
+
help='Generate only Semantic/Image counterfactuals (e.g. Change Color, Add Object); no Negative CFs')
|
| 1946 |
+
parser.add_argument('--negative_only', action='store_true',
|
| 1947 |
+
help='Generate only Negative counterfactuals (e.g. Change Lighting, Add Noise, Occlusion Change); no Semantic CFs')
|
| 1948 |
+
parser.add_argument('--same_cf_type', action='store_true',
|
| 1949 |
+
help='Use the same counterfactual type for all variants (first in --cf_types, or one random type if --cf_types not set)')
|
| 1950 |
+
parser.add_argument('--min_cf_change_score', type=float, default=1.0,
|
| 1951 |
+
help='Minimum heuristic change score for a counterfactual to be accepted (retries until met). Increase for more noticeable CFs.')
|
| 1952 |
+
parser.add_argument('--max_cf_attempts', type=int, default=10,
|
| 1953 |
+
help='Max retries per counterfactual type to meet --min_cf_change_score (default: 10)')
|
| 1954 |
+
parser.add_argument('--min_noise_level', type=str, default='light',
|
| 1955 |
+
choices=['light', 'medium', 'heavy'],
|
| 1956 |
+
help='Minimum noise level when using add_noise counterfactual (default: light)')
|
| 1957 |
+
|
| 1958 |
+
parser.add_argument('--list_cf_types', action='store_true',
|
| 1959 |
+
help='List all available counterfactual types and exit')
|
| 1960 |
+
|
| 1961 |
+
parser.add_argument('--generate_questions', action='store_true',
|
| 1962 |
+
help='Create questions and answers CSV after rendering completes')
|
| 1963 |
+
parser.add_argument('--filter_same_answer', action='store_true',
|
| 1964 |
+
help='After generating questions, remove scenes where CF1 or CF2 answer matches original (delete those rows and their image/scene files). Use with --generate_questions.')
|
| 1965 |
+
parser.add_argument('--csv_name', default='image_mapping_with_questions.csv',
|
| 1966 |
+
help='Output CSV filename (default: image_mapping_with_questions.csv)')
|
| 1967 |
+
parser.add_argument('--no_strict_validation', action='store_true',
|
| 1968 |
+
help='Disable strict question validation (Semantic-Valid / Negative-Valid); use legacy accept logic when generating questions CSV')
|
| 1969 |
+
parser.add_argument('--regenerate', nargs='+', type=int, metavar='N',
|
| 1970 |
+
help='Regenerate specific scene sets by index (e.g. --regenerate 63 83 272). Requires --run_name. Uses settings from run_metadata.json.')
|
| 1971 |
+
|
| 1972 |
+
args = parser.parse_args()
|
| 1973 |
+
|
| 1974 |
+
if args.list_cf_types:
|
| 1975 |
+
list_counterfactual_types()
|
| 1976 |
+
return
|
| 1977 |
+
|
| 1978 |
+
if args.render_only:
|
| 1979 |
+
render_existing_scenes(args)
|
| 1980 |
+
return
|
| 1981 |
+
|
| 1982 |
+
if args.resume and not args.run_name:
|
| 1983 |
+
print("ERROR: --run_name is required when using --resume")
|
| 1984 |
+
return
|
| 1985 |
+
|
| 1986 |
+
if args.regenerate is not None:
|
| 1987 |
+
if not args.run_name:
|
| 1988 |
+
print("ERROR: --run_name is required when using --regenerate")
|
| 1989 |
+
return
|
| 1990 |
+
regenerate_scene_sets(args)
|
| 1991 |
+
return
|
| 1992 |
+
|
| 1993 |
+
blender_path = args.blender_path or find_blender()
|
| 1994 |
+
print(f"Using Blender: {blender_path}")
|
| 1995 |
+
|
| 1996 |
+
print("\nPreparing scripts...")
|
| 1997 |
+
create_patched_render_script()
|
| 1998 |
+
run_dir = create_run_directory(args.output_dir, args.run_name)
|
| 1999 |
+
temp_run_id = os.path.basename(run_dir)
|
| 2000 |
+
print(f"\n{'='*70}")
|
| 2001 |
+
print(f"RUN DIRECTORY: {run_dir}")
|
| 2002 |
+
print(f"{'='*70}")
|
| 2003 |
+
|
| 2004 |
+
scenes_dir = os.path.join(run_dir, 'scenes')
|
| 2005 |
+
images_dir = os.path.join(run_dir, 'images')
|
| 2006 |
+
os.makedirs(scenes_dir, exist_ok=True)
|
| 2007 |
+
os.makedirs(images_dir, exist_ok=True)
|
| 2008 |
+
|
| 2009 |
+
checkpoint_file = os.path.join(run_dir, 'checkpoint.json')
|
| 2010 |
+
|
| 2011 |
+
completed_scenes = set()
|
| 2012 |
+
if args.resume:
|
| 2013 |
+
completed_scenes = load_checkpoint(checkpoint_file)
|
| 2014 |
+
rendered_scenes = get_completed_scenes_from_folder(images_dir)
|
| 2015 |
+
completed_scenes.update(rendered_scenes)
|
| 2016 |
+
|
| 2017 |
+
if completed_scenes:
|
| 2018 |
+
print(f"\n[RESUME] Found {len(completed_scenes)} already completed scenes")
|
| 2019 |
+
print(f" Completed: {sorted(completed_scenes)}")
|
| 2020 |
+
print(f" Will generate scenes: {args.num_scenes}")
|
| 2021 |
+
print(f" Remaining: {args.num_scenes - len(completed_scenes)}")
|
| 2022 |
+
else:
|
| 2023 |
+
print("\n[WARNING] Resume flag set but no checkpoint found, starting fresh")
|
| 2024 |
+
|
| 2025 |
+
print("\n" + "="*70)
|
| 2026 |
+
print(f"GENERATING {args.num_scenes} SCENE SETS")
|
| 2027 |
+
print(f"Each with {args.num_counterfactuals} counterfactual variants")
|
| 2028 |
+
print(f"Available CF types: {len(COUNTERFACTUAL_TYPES)}")
|
| 2029 |
+
print("="*70)
|
| 2030 |
+
|
| 2031 |
+
successful_scenes = 0
|
| 2032 |
+
successful_renders = 0
|
| 2033 |
+
|
| 2034 |
+
for i in range(args.num_scenes):
|
| 2035 |
+
if i in completed_scenes:
|
| 2036 |
+
print(f"\n[SKIP] Skipping scene {i} (already completed)")
|
| 2037 |
+
successful_scenes += 1
|
| 2038 |
+
successful_renders += 1
|
| 2039 |
+
continue
|
| 2040 |
+
|
| 2041 |
+
print(f"\n{'='*70}")
|
| 2042 |
+
print(f"SCENE SET {i+1}/{args.num_scenes} (Scene #{i})")
|
| 2043 |
+
print(f"{'='*70}")
|
| 2044 |
+
|
| 2045 |
+
if args.num_objects is not None:
|
| 2046 |
+
num_objects = args.num_objects
|
| 2047 |
+
else:
|
| 2048 |
+
num_objects = random.randint(args.min_objects, args.max_objects)
|
| 2049 |
+
|
| 2050 |
+
base_scene = None
|
| 2051 |
+
for retry in range(3):
|
| 2052 |
+
base_scene = generate_base_scene(num_objects, blender_path, i, temp_run_dir=temp_run_id)
|
| 2053 |
+
if base_scene and len(base_scene['objects']) > 0:
|
| 2054 |
+
break
|
| 2055 |
+
print(f" Retry {retry + 1}/3...")
|
| 2056 |
+
|
| 2057 |
+
if not base_scene or len(base_scene['objects']) == 0:
|
| 2058 |
+
print(f" [FAILED] Failed to generate scene {i+1}")
|
| 2059 |
+
continue
|
| 2060 |
+
|
| 2061 |
+
successful_scenes += 1
|
| 2062 |
+
|
| 2063 |
+
print(f" Creating {args.num_counterfactuals} counterfactuals...")
|
| 2064 |
+
counterfactuals = generate_counterfactuals(
|
| 2065 |
+
base_scene,
|
| 2066 |
+
args.num_counterfactuals,
|
| 2067 |
+
cf_types=args.cf_types,
|
| 2068 |
+
same_cf_type=args.same_cf_type,
|
| 2069 |
+
min_change_score=args.min_cf_change_score,
|
| 2070 |
+
max_cf_attempts=args.max_cf_attempts,
|
| 2071 |
+
min_noise_level=args.min_noise_level,
|
| 2072 |
+
semantic_only=args.semantic_only,
|
| 2073 |
+
negative_only=args.negative_only
|
| 2074 |
+
)
|
| 2075 |
+
|
| 2076 |
+
for idx, cf in enumerate(counterfactuals):
|
| 2077 |
+
cf_cat = cf.get('cf_category', 'unknown')
|
| 2078 |
+
print(f" CF{idx+1} [{cf_cat}] ({cf['type']}): {cf['description']}")
|
| 2079 |
+
|
| 2080 |
+
scene_prefix = f"scene_{i:04d}"
|
| 2081 |
+
scene_paths = {'original': os.path.join(scenes_dir, f"{scene_prefix}_original.json")}
|
| 2082 |
+
image_paths = {'original': os.path.join(images_dir, f"{scene_prefix}_original.png")}
|
| 2083 |
+
|
| 2084 |
+
base_scene['cf_metadata'] = {
|
| 2085 |
+
'variant': 'original',
|
| 2086 |
+
'is_counterfactual': False,
|
| 2087 |
+
'cf_index': None,
|
| 2088 |
+
'cf_category': 'original',
|
| 2089 |
+
'cf_type': None,
|
| 2090 |
+
'cf_description': None,
|
| 2091 |
+
'source_scene': scene_prefix,
|
| 2092 |
+
}
|
| 2093 |
+
save_scene(base_scene, scene_paths['original'])
|
| 2094 |
+
|
| 2095 |
+
for idx, cf in enumerate(counterfactuals):
|
| 2096 |
+
cf_name = f"cf{idx+1}"
|
| 2097 |
+
scene_paths[cf_name] = os.path.join(scenes_dir, f"{scene_prefix}_{cf_name}.json")
|
| 2098 |
+
image_paths[cf_name] = os.path.join(images_dir, f"{scene_prefix}_{cf_name}.png")
|
| 2099 |
+
|
| 2100 |
+
cf_scene = cf['scene']
|
| 2101 |
+
cf_scene['cf_metadata'] = {
|
| 2102 |
+
'variant': cf_name,
|
| 2103 |
+
'is_counterfactual': True,
|
| 2104 |
+
'cf_index': idx + 1,
|
| 2105 |
+
'cf_category': cf.get('cf_category', 'unknown'),
|
| 2106 |
+
'cf_type': cf.get('type', None),
|
| 2107 |
+
'cf_description': cf.get('description', None),
|
| 2108 |
+
'change_score': cf.get('change_score', None),
|
| 2109 |
+
'change_attempts': cf.get('change_attempts', None),
|
| 2110 |
+
'source_scene': scene_prefix,
|
| 2111 |
+
}
|
| 2112 |
+
save_scene(cf_scene, scene_paths[cf_name])
|
| 2113 |
+
|
| 2114 |
+
print(f" [OK] Saved {len(counterfactuals) + 1} scene files")
|
| 2115 |
+
|
| 2116 |
+
if not args.skip_render:
|
| 2117 |
+
print(" Rendering...")
|
| 2118 |
+
render_success = 0
|
| 2119 |
+
total_to_render = len(counterfactuals) + 1
|
| 2120 |
+
|
| 2121 |
+
for scene_type, scene_path in scene_paths.items():
|
| 2122 |
+
if render_scene(blender_path, scene_path, image_paths[scene_type],
|
| 2123 |
+
args.use_gpu, args.samples, args.width, args.height):
|
| 2124 |
+
render_success += 1
|
| 2125 |
+
print(f" [OK] {scene_type}")
|
| 2126 |
+
|
| 2127 |
+
if render_success == total_to_render:
|
| 2128 |
+
successful_renders += 1
|
| 2129 |
+
completed_scenes.add(i)
|
| 2130 |
+
save_checkpoint(checkpoint_file, list(completed_scenes))
|
| 2131 |
+
|
| 2132 |
+
print(f" [OK] Rendered {render_success}/{total_to_render} images")
|
| 2133 |
+
else:
|
| 2134 |
+
completed_scenes.add(i)
|
| 2135 |
+
save_checkpoint(checkpoint_file, list(completed_scenes))
|
| 2136 |
+
|
| 2137 |
+
save_run_metadata(run_dir, args, successful_scenes, successful_renders)
|
| 2138 |
+
|
| 2139 |
+
archive_path = create_downloadable_archive(run_dir)
|
| 2140 |
+
|
| 2141 |
+
temp_run_path = os.path.join(os.getcwd(), 'temp_output', temp_run_id)
|
| 2142 |
+
if os.path.exists(temp_run_path):
|
| 2143 |
+
shutil.rmtree(temp_run_path)
|
| 2144 |
+
if os.path.exists('render_images_patched.py'):
|
| 2145 |
+
os.remove('render_images_patched.py')
|
| 2146 |
+
|
| 2147 |
+
print("\n" + "="*70)
|
| 2148 |
+
print("PIPELINE COMPLETE")
|
| 2149 |
+
print("="*70)
|
| 2150 |
+
print(f"Run directory: {run_dir}")
|
| 2151 |
+
print(f"Successfully generated: {successful_scenes}/{args.num_scenes} scene sets")
|
| 2152 |
+
if not args.skip_render:
|
| 2153 |
+
print(f"Successfully rendered: {successful_renders}/{successful_scenes} scene sets")
|
| 2154 |
+
print(f"\nOutput:")
|
| 2155 |
+
print(f" Scene files: {scenes_dir}/")
|
| 2156 |
+
if not args.skip_render:
|
| 2157 |
+
print(f" Images: {images_dir}/")
|
| 2158 |
+
print(f" Metadata: {os.path.join(run_dir, 'run_metadata.json')}")
|
| 2159 |
+
print(f" Checkpoint: {checkpoint_file}")
|
| 2160 |
+
if archive_path:
|
| 2161 |
+
print(f"\n[ARCHIVE] Downloadable archive created:")
|
| 2162 |
+
print(f" Filename: {os.path.basename(archive_path)}")
|
| 2163 |
+
print(f" Location: {os.path.abspath(archive_path)}")
|
| 2164 |
+
print(f" To download: Go to the 'Files' tab in your Hugging Face Space")
|
| 2165 |
+
print(f" and look for '{os.path.basename(archive_path)}' in the file list")
|
| 2166 |
+
print(f"\nFile naming: scene_XXXX_original/cf1/cf2/....json/png")
|
| 2167 |
+
if args.cf_types:
|
| 2168 |
+
print(f"\nCounterfactual types requested: {', '.join(args.cf_types)}")
|
| 2169 |
+
else:
|
| 2170 |
+
print("\nCounterfactual types: default (mix of image_cf + negative_cf)")
|
| 2171 |
+
|
| 2172 |
+
if args.resume and completed_scenes:
|
| 2173 |
+
print(f"\n[OK] Resume successful! Completed {len(completed_scenes)}/{args.num_scenes} scenes total")
|
| 2174 |
+
|
| 2175 |
+
if args.generate_questions and not args.skip_render:
|
| 2176 |
+
if generate_mapping_with_questions is None:
|
| 2177 |
+
print("\n[WARNING] Questions module not found. Skipping.")
|
| 2178 |
+
else:
|
| 2179 |
+
print("\n" + "="*70)
|
| 2180 |
+
print("QUESTIONS AND ANSWERS")
|
| 2181 |
+
print("="*70)
|
| 2182 |
+
try:
|
| 2183 |
+
generate_mapping_with_questions(
|
| 2184 |
+
run_dir,
|
| 2185 |
+
args.csv_name if args.num_counterfactuals != 1 else 'image_mapping_single_cf.csv',
|
| 2186 |
+
generate_questions=True,
|
| 2187 |
+
strict_question_validation=not getattr(args, 'no_strict_validation', False),
|
| 2188 |
+
single_cf_per_row=(args.num_counterfactuals == 1)
|
| 2189 |
+
)
|
| 2190 |
+
csv_used = args.csv_name if args.num_counterfactuals != 1 else 'image_mapping_single_cf.csv'
|
| 2191 |
+
print(f"\n[OK] CSV saved to: {os.path.join(run_dir, csv_used)}")
|
| 2192 |
+
if getattr(args, 'filter_same_answer', False):
|
| 2193 |
+
filter_same_answer_scenes(run_dir, csv_used)
|
| 2194 |
+
except Exception as e:
|
| 2195 |
+
print(f"\n[ERROR] Questions: {e}")
|
| 2196 |
+
import traceback
|
| 2197 |
+
traceback.print_exc()
|
| 2198 |
+
|
| 2199 |
+
if __name__ == '__main__':
|
| 2200 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core dependencies
|
| 2 |
+
numpy>=1.24.0
|
| 3 |
+
Pillow>=10.0.0
|
| 4 |
+
|
| 5 |
+
# Streamlit for UI
|
| 6 |
+
streamlit>=1.28.0
|
| 7 |
+
|
| 8 |
+
# PyTorch and Hugging Face dependencies
|
| 9 |
+
# Using flexible versions for compatibility with Python 3.13
|
| 10 |
+
torch>=2.0.0
|
| 11 |
+
transformers>=4.35.0
|
| 12 |
+
huggingface_hub>=0.16.4,<0.18
|
scripts/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Scripts
|
| 2 |
+
|
| 3 |
+
Utility scripts for working with CLEVR counterfactual scenes.
|
| 4 |
+
|
| 5 |
+
## Available Scripts
|
| 6 |
+
|
| 7 |
+
### generate_scenes.py
|
| 8 |
+
Generate scene JSON files with counterfactuals (no rendering).
|
| 9 |
+
|
| 10 |
+
**Purpose**: Create scene JSON files that can be rendered later.
|
| 11 |
+
|
| 12 |
+
**Usage:**
|
| 13 |
+
```bash
|
| 14 |
+
# Generate 10 scene sets
|
| 15 |
+
python scripts/generate_scenes.py --num_scenes 10 --num_objects 5 --run_name experiment1
|
| 16 |
+
|
| 17 |
+
# Resume from checkpoint
|
| 18 |
+
python scripts/generate_scenes.py --num_scenes 100 --run_name experiment1 --resume
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
**Output**: Scene JSON files in `output/run_name/scenes/`
|
| 22 |
+
|
| 23 |
+
**Next step**: Use `pipeline.py --render_only` to render these scenes.
|
| 24 |
+
|
| 25 |
+
**Note**: Alternatively, you can use `pipeline.py --skip_render` instead of this script.
|
| 26 |
+
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
### generate_examples.py
|
| 30 |
+
Generate examples of each counterfactual type applied to a base scene. Optionally renders scenes to images.
|
| 31 |
+
|
| 32 |
+
**Purpose**: Create reference examples demonstrating all counterfactual types.
|
| 33 |
+
|
| 34 |
+
**Usage:**
|
| 35 |
+
```bash
|
| 36 |
+
# Generate scene JSON files only
|
| 37 |
+
python scripts/generate_examples.py [--output_dir DIR] [--num_objects N]
|
| 38 |
+
|
| 39 |
+
# Generate and render to images
|
| 40 |
+
python scripts/generate_examples.py --render [--output_dir DIR] [--num_objects N] [--use_gpu 0|1]
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
**Options:**
|
| 44 |
+
- `--output_dir`: Output directory (default: `output/counterfactual_examples`)
|
| 45 |
+
- `--num_objects`: Number of objects in base scene (default: 5)
|
| 46 |
+
- `--render`: Render scenes to PNG images
|
| 47 |
+
- `--use_gpu`: Use GPU rendering (0 = CPU, 1 = GPU, default: 0)
|
| 48 |
+
|
| 49 |
+
**Output**:
|
| 50 |
+
- Scene JSON files for all counterfactual types
|
| 51 |
+
- Optional: PNG images (if `--render` is used)
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
### generate_questions_mapping.py
|
| 56 |
+
Generate CSV mapping with questions and counterfactual questions for scenes.
|
| 57 |
+
|
| 58 |
+
**Purpose**: Create question-answer datasets for training/evaluation.
|
| 59 |
+
|
| 60 |
+
**Usage:**
|
| 61 |
+
```bash
|
| 62 |
+
# For a specific run directory
|
| 63 |
+
python scripts/generate_questions_mapping.py --output_dir output/experiment1 --generate_questions
|
| 64 |
+
|
| 65 |
+
# Auto-detect latest run
|
| 66 |
+
python scripts/generate_questions_mapping.py --output_dir output --auto_latest --generate_questions
|
| 67 |
+
|
| 68 |
+
# Generate CSV with scene_id and links (relative paths)
|
| 69 |
+
python scripts/generate_questions_mapping.py --output_dir output/experiment1 --generate_questions --with_links
|
| 70 |
+
|
| 71 |
+
# Generate CSV with scene_id and full URLs
|
| 72 |
+
python scripts/generate_questions_mapping.py --output_dir output/experiment1 --generate_questions --with_links --base_url https://example.com/dataset
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
**Options:**
|
| 76 |
+
- `--output_dir`: Run directory or base output directory (default: `output`)
|
| 77 |
+
- `--auto_latest`: Automatically find and use the latest run in output_dir
|
| 78 |
+
- `--csv_name`: Output CSV filename (default: `image_mapping_with_questions.csv`)
|
| 79 |
+
- `--generate_questions`: Generate questions and answers for each scene set
|
| 80 |
+
- `--with_links`: Include scene_id and image/scene link columns (for URLs or file paths)
|
| 81 |
+
- `--base_url`: Base URL for links (e.g., `https://example.com`). If not provided, uses relative paths like `images/filename.png`
|
| 82 |
+
|
| 83 |
+
**Output**: CSV files with question-answer mappings
|
| 84 |
+
|
| 85 |
+
---
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
## Main Pipeline
|
| 89 |
+
|
| 90 |
+
For production use (generating large datasets), use the main pipeline script:
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
python pipeline.py --num_scenes 100 --num_objects 5 --run_name my_experiment
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
See the main `README.md` for full documentation of the production pipeline.
|
| 97 |
+
|
| 98 |
+
---
|
| 99 |
+
|
| 100 |
+
## Script Summary
|
| 101 |
+
|
| 102 |
+
| Script | Purpose | When to Use |
|
| 103 |
+
|--------|---------|-------------|
|
| 104 |
+
| `generate_scenes.py` | Generate scene JSON files | Generate scenes separately (alternative to `pipeline.py --skip_render`) |
|
| 105 |
+
| `generate_examples.py` | Generate reference examples | Creating demonstrations, testing counterfactuals |
|
| 106 |
+
| `generate_questions_mapping.py` | Create QA datasets | Preparing training/evaluation data |
|
| 107 |
+
| `pipeline.py` | Combined generation + rendering | Main entry point. Supports `--skip_render` and `--render_only` modes |
|
scripts/__pycache__/generate_examples.cpython-312.pyc
ADDED
|
Binary file (9.84 kB). View file
|
|
|
scripts/__pycache__/generate_questions_mapping.cpython-312.pyc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:61369dbc63eb9bd066d8a9fe9d5f0ac6f1f57645ea4705ec163aadaf1665ef66
|
| 3 |
+
size 103022
|
scripts/__pycache__/generate_scenes.cpython-312.pyc
ADDED
|
Binary file (10.9 kB). View file
|
|
|
scripts/__pycache__/render.cpython-312.pyc
ADDED
|
Binary file (53.9 kB). View file
|
|
|
scripts/generate_examples.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
import copy
|
| 7 |
+
import argparse
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 11 |
+
project_root = os.path.dirname(script_dir)
|
| 12 |
+
sys.path.insert(0, project_root)
|
| 13 |
+
|
| 14 |
+
from pipeline import (
|
| 15 |
+
generate_base_scene,
|
| 16 |
+
find_blender,
|
| 17 |
+
create_patched_utils,
|
| 18 |
+
create_patched_render_script,
|
| 19 |
+
create_render_from_json_script,
|
| 20 |
+
save_scene,
|
| 21 |
+
render_scene,
|
| 22 |
+
IMAGE_COUNTERFACTUALS,
|
| 23 |
+
NEGATIVE_COUNTERFACTUALS,
|
| 24 |
+
COUNTERFACTUAL_TYPES
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
def generate_all_counterfactual_examples(output_dir='output/counterfactual_examples',
|
| 28 |
+
num_objects=5,
|
| 29 |
+
render=False,
|
| 30 |
+
use_gpu=0):
|
| 31 |
+
print("="*70)
|
| 32 |
+
print("GENERATING COUNTERFACTUAL EXAMPLES")
|
| 33 |
+
print("="*70)
|
| 34 |
+
|
| 35 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 36 |
+
project_root = os.path.dirname(script_dir)
|
| 37 |
+
if not os.path.isabs(output_dir):
|
| 38 |
+
output_dir = os.path.join(project_root, output_dir)
|
| 39 |
+
|
| 40 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 41 |
+
scenes_dir = os.path.join(output_dir, 'scenes')
|
| 42 |
+
images_dir = os.path.join(output_dir, 'images') if render else None
|
| 43 |
+
if render:
|
| 44 |
+
os.makedirs(images_dir, exist_ok=True)
|
| 45 |
+
os.makedirs(scenes_dir, exist_ok=True)
|
| 46 |
+
|
| 47 |
+
blender_path = find_blender()
|
| 48 |
+
create_patched_utils()
|
| 49 |
+
create_patched_render_script()
|
| 50 |
+
if render:
|
| 51 |
+
create_render_from_json_script()
|
| 52 |
+
|
| 53 |
+
print(f"\n1. Generating base scene with {num_objects} objects...")
|
| 54 |
+
base_scene = None
|
| 55 |
+
for retry in range(3):
|
| 56 |
+
base_scene = generate_base_scene(num_objects, blender_path, 0)
|
| 57 |
+
if base_scene and len(base_scene.get('objects', [])) > 0:
|
| 58 |
+
break
|
| 59 |
+
print(f" Retry {retry + 1}/3...")
|
| 60 |
+
|
| 61 |
+
if not base_scene or len(base_scene.get('objects', [])) == 0:
|
| 62 |
+
print("ERROR: Failed to generate base scene")
|
| 63 |
+
return
|
| 64 |
+
|
| 65 |
+
original_path = os.path.join(scenes_dir, '00_original.json')
|
| 66 |
+
save_scene(base_scene, original_path)
|
| 67 |
+
print(f" [OK] Saved original scene: {original_path}")
|
| 68 |
+
|
| 69 |
+
if render:
|
| 70 |
+
original_image = os.path.join(images_dir, '00_original.png')
|
| 71 |
+
print(f" Rendering original scene...")
|
| 72 |
+
render_scene(blender_path, original_path, original_image, use_gpu=use_gpu)
|
| 73 |
+
if os.path.exists(original_image):
|
| 74 |
+
print(f" [OK] Rendered: 00_original.png")
|
| 75 |
+
|
| 76 |
+
all_cf_types = {**IMAGE_COUNTERFACTUALS, **NEGATIVE_COUNTERFACTUALS}
|
| 77 |
+
total_cfs = len(all_cf_types)
|
| 78 |
+
|
| 79 |
+
print(f"\n2. Generating {total_cfs} counterfactual examples...")
|
| 80 |
+
|
| 81 |
+
image_cf_index = 1
|
| 82 |
+
for cf_type, cf_func in sorted(IMAGE_COUNTERFACTUALS.items()):
|
| 83 |
+
print(f"\n [Image CF {image_cf_index}/{len(IMAGE_COUNTERFACTUALS)}] Applying: {cf_type}")
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
cf_scene, description = cf_func(copy.deepcopy(base_scene))
|
| 87 |
+
|
| 88 |
+
cf_scene['cf_metadata'] = {
|
| 89 |
+
'variant': f'image_cf_{image_cf_index}',
|
| 90 |
+
'is_counterfactual': True,
|
| 91 |
+
'cf_index': image_cf_index,
|
| 92 |
+
'cf_category': 'image_cf',
|
| 93 |
+
'cf_type': cf_type,
|
| 94 |
+
'cf_description': description,
|
| 95 |
+
'source_scene': '00_original'
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
filename = f"01_image_{image_cf_index:02d}_{cf_type}.json"
|
| 99 |
+
cf_path = os.path.join(scenes_dir, filename)
|
| 100 |
+
|
| 101 |
+
save_scene(cf_scene, cf_path)
|
| 102 |
+
print(f" [OK] Saved: {filename}")
|
| 103 |
+
print(f" Description: {description}")
|
| 104 |
+
|
| 105 |
+
if render:
|
| 106 |
+
cf_image = os.path.join(images_dir, filename.replace('.json', '.png'))
|
| 107 |
+
render_scene(blender_path, cf_path, cf_image, use_gpu=use_gpu)
|
| 108 |
+
if os.path.exists(cf_image):
|
| 109 |
+
print(f" [OK] Rendered: {filename.replace('.json', '.png')}")
|
| 110 |
+
|
| 111 |
+
image_cf_index += 1
|
| 112 |
+
|
| 113 |
+
except Exception as e:
|
| 114 |
+
print(f" [ERROR] ERROR applying {cf_type}: {e}")
|
| 115 |
+
import traceback
|
| 116 |
+
traceback.print_exc()
|
| 117 |
+
continue
|
| 118 |
+
|
| 119 |
+
negative_cf_index = 1
|
| 120 |
+
for cf_type, cf_func in sorted(NEGATIVE_COUNTERFACTUALS.items()):
|
| 121 |
+
print(f"\n [Negative CF {negative_cf_index}/{len(NEGATIVE_COUNTERFACTUALS)}] Applying: {cf_type}")
|
| 122 |
+
|
| 123 |
+
try:
|
| 124 |
+
if cf_type == 'add_noise':
|
| 125 |
+
cf_scene, description = cf_func(copy.deepcopy(base_scene), min_noise_level='medium')
|
| 126 |
+
else:
|
| 127 |
+
cf_scene, description = cf_func(copy.deepcopy(base_scene))
|
| 128 |
+
|
| 129 |
+
cf_scene['cf_metadata'] = {
|
| 130 |
+
'variant': f'negative_cf_{negative_cf_index}',
|
| 131 |
+
'is_counterfactual': True,
|
| 132 |
+
'cf_index': negative_cf_index,
|
| 133 |
+
'cf_category': 'negative_cf',
|
| 134 |
+
'cf_type': cf_type,
|
| 135 |
+
'cf_description': description,
|
| 136 |
+
'source_scene': '00_original'
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
filename = f"02_negative_{negative_cf_index:02d}_{cf_type}.json"
|
| 140 |
+
cf_path = os.path.join(scenes_dir, filename)
|
| 141 |
+
|
| 142 |
+
save_scene(cf_scene, cf_path)
|
| 143 |
+
print(f" [OK] Saved: {filename}")
|
| 144 |
+
print(f" Description: {description}")
|
| 145 |
+
|
| 146 |
+
if render:
|
| 147 |
+
cf_image = os.path.join(images_dir, filename.replace('.json', '.png'))
|
| 148 |
+
render_scene(blender_path, cf_path, cf_image, use_gpu=use_gpu)
|
| 149 |
+
if os.path.exists(cf_image):
|
| 150 |
+
print(f" [OK] Rendered: {filename.replace('.json', '.png')}")
|
| 151 |
+
|
| 152 |
+
negative_cf_index += 1
|
| 153 |
+
|
| 154 |
+
except Exception as e:
|
| 155 |
+
print(f" [ERROR] ERROR applying {cf_type}: {e}")
|
| 156 |
+
import traceback
|
| 157 |
+
traceback.print_exc()
|
| 158 |
+
continue
|
| 159 |
+
|
| 160 |
+
summary = {
|
| 161 |
+
'base_scene': '00_original.json',
|
| 162 |
+
'total_counterfactuals': len(all_cf_types),
|
| 163 |
+
'image_counterfactuals': len(IMAGE_COUNTERFACTUALS),
|
| 164 |
+
'negative_counterfactuals': len(NEGATIVE_COUNTERFACTUALS),
|
| 165 |
+
'counterfactuals': {}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
image_cf_index = 1
|
| 169 |
+
for cf_type in sorted(IMAGE_COUNTERFACTUALS.keys()):
|
| 170 |
+
filename = f"01_image_{image_cf_index:02d}_{cf_type}.json"
|
| 171 |
+
summary['counterfactuals'][cf_type] = {
|
| 172 |
+
'filename': filename,
|
| 173 |
+
'category': 'image_cf',
|
| 174 |
+
'index': image_cf_index
|
| 175 |
+
}
|
| 176 |
+
image_cf_index += 1
|
| 177 |
+
|
| 178 |
+
negative_cf_index = 1
|
| 179 |
+
for cf_type in sorted(NEGATIVE_COUNTERFACTUALS.keys()):
|
| 180 |
+
filename = f"02_negative_{negative_cf_index:02d}_{cf_type}.json"
|
| 181 |
+
summary['counterfactuals'][cf_type] = {
|
| 182 |
+
'filename': filename,
|
| 183 |
+
'category': 'negative_cf',
|
| 184 |
+
'index': negative_cf_index
|
| 185 |
+
}
|
| 186 |
+
negative_cf_index += 1
|
| 187 |
+
|
| 188 |
+
summary_path = os.path.join(output_dir, 'summary.json')
|
| 189 |
+
with open(summary_path, 'w') as f:
|
| 190 |
+
json.dump(summary, f, indent=2)
|
| 191 |
+
|
| 192 |
+
print("\n" + "="*70)
|
| 193 |
+
print("SUMMARY")
|
| 194 |
+
print("="*70)
|
| 195 |
+
print(f"Base scene: {original_path}")
|
| 196 |
+
print(f"Total counterfactuals generated: {len(all_cf_types)}")
|
| 197 |
+
print(f" - Image CFs: {len(IMAGE_COUNTERFACTUALS)}")
|
| 198 |
+
print(f" - Negative CFs: {len(NEGATIVE_COUNTERFACTUALS)}")
|
| 199 |
+
print(f"\nAll files saved to: {output_dir}")
|
| 200 |
+
print(f"Summary saved to: {summary_path}")
|
| 201 |
+
if render:
|
| 202 |
+
print(f"Images saved to: {images_dir}")
|
| 203 |
+
print("="*70)
|
| 204 |
+
|
| 205 |
+
if __name__ == '__main__':
|
| 206 |
+
parser = argparse.ArgumentParser(description='Generate examples of each counterfactual type')
|
| 207 |
+
parser.add_argument('--output_dir', type=str, default='output/counterfactual_examples',
|
| 208 |
+
help='Output directory for examples (default: output/counterfactual_examples)')
|
| 209 |
+
parser.add_argument('--num_objects', type=int, default=5,
|
| 210 |
+
help='Number of objects in base scene (default: 5)')
|
| 211 |
+
parser.add_argument('--render', action='store_true',
|
| 212 |
+
help='Render scenes to images')
|
| 213 |
+
parser.add_argument('--use_gpu', type=int, default=0,
|
| 214 |
+
help='Use GPU rendering (0 = CPU, 1 = GPU, default: 0)')
|
| 215 |
+
|
| 216 |
+
args = parser.parse_args()
|
| 217 |
+
|
| 218 |
+
generate_all_counterfactual_examples(args.output_dir, args.num_objects, args.render, args.use_gpu)
|
scripts/generate_questions_mapping.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
scripts/generate_scenes.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 7 |
+
project_root = os.path.dirname(script_dir)
|
| 8 |
+
sys.path.insert(0, project_root)
|
| 9 |
+
|
| 10 |
+
from pipeline import (
|
| 11 |
+
find_blender,
|
| 12 |
+
create_patched_render_script,
|
| 13 |
+
create_run_directory,
|
| 14 |
+
generate_base_scene,
|
| 15 |
+
generate_counterfactuals,
|
| 16 |
+
save_scene,
|
| 17 |
+
save_checkpoint,
|
| 18 |
+
load_checkpoint,
|
| 19 |
+
get_completed_scenes_from_folder,
|
| 20 |
+
COUNTERFACTUAL_TYPES,
|
| 21 |
+
list_counterfactual_types
|
| 22 |
+
)
|
| 23 |
+
import argparse
|
| 24 |
+
import random
|
| 25 |
+
import json
|
| 26 |
+
import shutil
|
| 27 |
+
from datetime import datetime
|
| 28 |
+
|
| 29 |
+
def main():
|
| 30 |
+
parser = argparse.ArgumentParser(
|
| 31 |
+
description='Generate scene JSON files with counterfactuals (no rendering)'
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
parser.add_argument('--num_scenes', type=int, default=5,
|
| 35 |
+
help='Number of scene sets to generate')
|
| 36 |
+
parser.add_argument('--num_objects', type=int, default=None,
|
| 37 |
+
help='Fixed number of objects per scene (overrides min/max)')
|
| 38 |
+
parser.add_argument('--min_objects', type=int, default=3,
|
| 39 |
+
help='Minimum object count (if num_objects not given)')
|
| 40 |
+
parser.add_argument('--max_objects', type=int, default=7,
|
| 41 |
+
help='Maximum object count (if num_objects not given)')
|
| 42 |
+
parser.add_argument('--num_counterfactuals', type=int, default=2,
|
| 43 |
+
help='Number of counterfactual variants per scene')
|
| 44 |
+
parser.add_argument('--blender_path', type=str, default=None,
|
| 45 |
+
help='Path to Blender executable (auto-detected if not provided)')
|
| 46 |
+
parser.add_argument('--output_dir', type=str, default='output',
|
| 47 |
+
help='Base directory for all runs')
|
| 48 |
+
parser.add_argument('--run_name', type=str, default=None,
|
| 49 |
+
help='Optional custom name for this run')
|
| 50 |
+
parser.add_argument('--resume', action='store_true',
|
| 51 |
+
help='Resume from last checkpoint (requires --run_name)')
|
| 52 |
+
parser.add_argument('--cf_types', nargs='+',
|
| 53 |
+
choices=[
|
| 54 |
+
'change_color', 'change_shape', 'change_size',
|
| 55 |
+
'change_material', 'change_position',
|
| 56 |
+
'add_object', 'remove_object', 'swap_attribute', 'occlusion_change', 'relational_flip',
|
| 57 |
+
'replace_object',
|
| 58 |
+
'change_background',
|
| 59 |
+
'change_lighting', 'add_noise',
|
| 60 |
+
'apply_fisheye', 'apply_blur', 'apply_vignette', 'apply_chromatic_aberration'
|
| 61 |
+
],
|
| 62 |
+
help='Specific counterfactual types to use')
|
| 63 |
+
parser.add_argument('--semantic_only', action='store_true',
|
| 64 |
+
help='Generate only Semantic/Image counterfactuals (no Negative CFs)')
|
| 65 |
+
parser.add_argument('--negative_only', action='store_true',
|
| 66 |
+
help='Generate only Negative counterfactuals (no Semantic CFs)')
|
| 67 |
+
parser.add_argument('--same_cf_type', action='store_true',
|
| 68 |
+
help='Use the same counterfactual type for all variants')
|
| 69 |
+
parser.add_argument('--min_cf_change_score', type=float, default=1.0,
|
| 70 |
+
help='Minimum heuristic change score for counterfactuals')
|
| 71 |
+
parser.add_argument('--max_cf_attempts', type=int, default=10,
|
| 72 |
+
help='Max retries per counterfactual to meet --min_cf_change_score')
|
| 73 |
+
parser.add_argument('--min_noise_level', type=str, default='light',
|
| 74 |
+
choices=['light', 'medium', 'heavy'],
|
| 75 |
+
help='Minimum noise level when using add_noise counterfactual')
|
| 76 |
+
parser.add_argument('--list_cf_types', action='store_true',
|
| 77 |
+
help='List all available counterfactual types and exit')
|
| 78 |
+
|
| 79 |
+
args = parser.parse_args()
|
| 80 |
+
|
| 81 |
+
if args.list_cf_types:
|
| 82 |
+
list_counterfactual_types()
|
| 83 |
+
return
|
| 84 |
+
|
| 85 |
+
if args.resume and not args.run_name:
|
| 86 |
+
print("ERROR: --run_name is required when using --resume")
|
| 87 |
+
return
|
| 88 |
+
|
| 89 |
+
blender_path = args.blender_path or find_blender()
|
| 90 |
+
print(f"Using Blender: {blender_path}")
|
| 91 |
+
|
| 92 |
+
print("\nPreparing scripts...")
|
| 93 |
+
create_patched_render_script()
|
| 94 |
+
|
| 95 |
+
run_dir = create_run_directory(args.output_dir, args.run_name)
|
| 96 |
+
temp_run_id = os.path.basename(run_dir)
|
| 97 |
+
print(f"\n{'='*70}")
|
| 98 |
+
print(f"RUN DIRECTORY: {run_dir}")
|
| 99 |
+
print(f"{'='*70}")
|
| 100 |
+
|
| 101 |
+
scenes_dir = os.path.join(run_dir, 'scenes')
|
| 102 |
+
os.makedirs(scenes_dir, exist_ok=True)
|
| 103 |
+
|
| 104 |
+
checkpoint_file = os.path.join(run_dir, 'checkpoint.json')
|
| 105 |
+
|
| 106 |
+
completed_scenes = set()
|
| 107 |
+
if args.resume:
|
| 108 |
+
completed_scenes = load_checkpoint(checkpoint_file)
|
| 109 |
+
existing_scenes = get_completed_scenes_from_folder(scenes_dir)
|
| 110 |
+
completed_scenes.update(existing_scenes)
|
| 111 |
+
|
| 112 |
+
if completed_scenes:
|
| 113 |
+
print(f"\n[RESUME] Found {len(completed_scenes)} already completed scenes")
|
| 114 |
+
else:
|
| 115 |
+
print("\n[WARNING] Resume flag set but no checkpoint found, starting fresh")
|
| 116 |
+
|
| 117 |
+
print("\n" + "="*70)
|
| 118 |
+
print(f"GENERATING {args.num_scenes} SCENE SETS (JSON ONLY)")
|
| 119 |
+
print(f"Each with {args.num_counterfactuals} counterfactual variants")
|
| 120 |
+
print("="*70)
|
| 121 |
+
|
| 122 |
+
successful_scenes = 0
|
| 123 |
+
|
| 124 |
+
for i in range(args.num_scenes):
|
| 125 |
+
if i in completed_scenes:
|
| 126 |
+
print(f"\n[SKIP] Skipping scene {i} (already completed)")
|
| 127 |
+
successful_scenes += 1
|
| 128 |
+
continue
|
| 129 |
+
|
| 130 |
+
print(f"\n{'='*70}")
|
| 131 |
+
print(f"SCENE SET {i+1}/{args.num_scenes} (Scene #{i})")
|
| 132 |
+
print(f"{'='*70}")
|
| 133 |
+
|
| 134 |
+
if args.num_objects is not None:
|
| 135 |
+
num_objects = args.num_objects
|
| 136 |
+
else:
|
| 137 |
+
num_objects = random.randint(args.min_objects, args.max_objects)
|
| 138 |
+
|
| 139 |
+
base_scene = None
|
| 140 |
+
for retry in range(3):
|
| 141 |
+
base_scene = generate_base_scene(num_objects, blender_path, i, temp_run_dir=temp_run_id)
|
| 142 |
+
if base_scene and len(base_scene['objects']) > 0:
|
| 143 |
+
break
|
| 144 |
+
print(f" Retry {retry + 1}/3...")
|
| 145 |
+
|
| 146 |
+
if not base_scene or len(base_scene['objects']) == 0:
|
| 147 |
+
print(f" [FAILED] Failed to generate scene {i+1}")
|
| 148 |
+
continue
|
| 149 |
+
|
| 150 |
+
successful_scenes += 1
|
| 151 |
+
|
| 152 |
+
print(f" Creating {args.num_counterfactuals} counterfactuals...")
|
| 153 |
+
counterfactuals = generate_counterfactuals(
|
| 154 |
+
base_scene,
|
| 155 |
+
args.num_counterfactuals,
|
| 156 |
+
cf_types=args.cf_types,
|
| 157 |
+
same_cf_type=args.same_cf_type,
|
| 158 |
+
min_change_score=args.min_cf_change_score,
|
| 159 |
+
max_cf_attempts=args.max_cf_attempts,
|
| 160 |
+
min_noise_level=args.min_noise_level,
|
| 161 |
+
semantic_only=args.semantic_only,
|
| 162 |
+
negative_only=args.negative_only
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
for idx, cf in enumerate(counterfactuals):
|
| 166 |
+
cf_cat = cf.get('cf_category', 'unknown')
|
| 167 |
+
print(f" CF{idx+1} [{cf_cat}] ({cf['type']}): {cf['description']}")
|
| 168 |
+
|
| 169 |
+
scene_num = i + 1
|
| 170 |
+
scene_prefix = f"scene_{scene_num:04d}"
|
| 171 |
+
scene_paths = {'original': os.path.join(scenes_dir, f"{scene_prefix}_original.json")}
|
| 172 |
+
|
| 173 |
+
base_scene['cf_metadata'] = {
|
| 174 |
+
'variant': 'original',
|
| 175 |
+
'is_counterfactual': False,
|
| 176 |
+
'cf_index': None,
|
| 177 |
+
'cf_category': 'original',
|
| 178 |
+
'cf_type': None,
|
| 179 |
+
'cf_description': None,
|
| 180 |
+
'source_scene': scene_prefix,
|
| 181 |
+
}
|
| 182 |
+
save_scene(base_scene, scene_paths['original'])
|
| 183 |
+
|
| 184 |
+
for idx, cf in enumerate(counterfactuals):
|
| 185 |
+
cf_name = f"cf{idx+1}"
|
| 186 |
+
scene_paths[cf_name] = os.path.join(scenes_dir, f"{scene_prefix}_{cf_name}.json")
|
| 187 |
+
|
| 188 |
+
cf_scene = cf['scene']
|
| 189 |
+
cf_scene['cf_metadata'] = {
|
| 190 |
+
'variant': cf_name,
|
| 191 |
+
'is_counterfactual': True,
|
| 192 |
+
'cf_index': idx + 1,
|
| 193 |
+
'cf_category': cf.get('cf_category', 'unknown'),
|
| 194 |
+
'cf_type': cf.get('type', None),
|
| 195 |
+
'cf_description': cf.get('description', None),
|
| 196 |
+
'change_score': cf.get('change_score', None),
|
| 197 |
+
'change_attempts': cf.get('change_attempts', None),
|
| 198 |
+
'source_scene': scene_prefix,
|
| 199 |
+
}
|
| 200 |
+
save_scene(cf_scene, scene_paths[cf_name])
|
| 201 |
+
|
| 202 |
+
print(f" [OK] Saved {len(counterfactuals) + 1} scene files")
|
| 203 |
+
|
| 204 |
+
completed_scenes.add(i)
|
| 205 |
+
save_checkpoint(checkpoint_file, list(completed_scenes))
|
| 206 |
+
|
| 207 |
+
metadata = {
|
| 208 |
+
'timestamp': datetime.now().isoformat(),
|
| 209 |
+
'num_scenes': args.num_scenes,
|
| 210 |
+
'num_counterfactuals': args.num_counterfactuals,
|
| 211 |
+
'successful_scenes': successful_scenes,
|
| 212 |
+
'successful_renders': 0,
|
| 213 |
+
'cf_types': args.cf_types if args.cf_types else 'default',
|
| 214 |
+
'semantic_only': args.semantic_only,
|
| 215 |
+
'negative_only': args.negative_only,
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
metadata_path = os.path.join(run_dir, 'run_metadata.json')
|
| 219 |
+
with open(metadata_path, 'w') as f:
|
| 220 |
+
json.dump(metadata, f, indent=2)
|
| 221 |
+
|
| 222 |
+
temp_run_path = os.path.join(os.getcwd(), 'temp_output', temp_run_id)
|
| 223 |
+
if os.path.exists(temp_run_path):
|
| 224 |
+
shutil.rmtree(temp_run_path)
|
| 225 |
+
if os.path.exists('render_images_patched.py'):
|
| 226 |
+
os.remove('render_images_patched.py')
|
| 227 |
+
|
| 228 |
+
print("\n" + "="*70)
|
| 229 |
+
print("SCENE COMPLETE")
|
| 230 |
+
print("="*70)
|
| 231 |
+
print(f"Run directory: {run_dir}")
|
| 232 |
+
print(f"Successfully generated: {successful_scenes}/{args.num_scenes} scene sets")
|
| 233 |
+
print(f"\nOutput:")
|
| 234 |
+
print(f" Scene files: {scenes_dir}/")
|
| 235 |
+
print(f" Metadata: {metadata_path}")
|
| 236 |
+
print(f" Checkpoint: {checkpoint_file}")
|
| 237 |
+
print(f"\nNext step: Run 'python pipeline.py --render_only --run_name {args.run_name}' to render these scenes")
|
| 238 |
+
print("="*70)
|
| 239 |
+
|
| 240 |
+
if __name__ == '__main__':
|
| 241 |
+
main()
|
| 242 |
+
|
scripts/render.py
ADDED
|
@@ -0,0 +1,1204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import print_function
|
| 2 |
+
import math, sys, random, argparse, json, os, tempfile
|
| 3 |
+
from datetime import datetime as dt
|
| 4 |
+
from collections import Counter
|
| 5 |
+
try:
|
| 6 |
+
from PIL import Image, ImageFilter
|
| 7 |
+
except ImportError:
|
| 8 |
+
Image = None
|
| 9 |
+
ImageFilter = None
|
| 10 |
+
|
| 11 |
+
INSIDE_BLENDER = True
|
| 12 |
+
try:
|
| 13 |
+
import bpy, bpy_extras
|
| 14 |
+
from mathutils import Vector
|
| 15 |
+
except ImportError as e:
|
| 16 |
+
INSIDE_BLENDER = False
|
| 17 |
+
|
| 18 |
+
def extract_args(input_argv=None):
|
| 19 |
+
if input_argv is None:
|
| 20 |
+
input_argv = sys.argv
|
| 21 |
+
output_argv = []
|
| 22 |
+
if '--' in input_argv:
|
| 23 |
+
idx = input_argv.index('--')
|
| 24 |
+
output_argv = input_argv[(idx + 1):]
|
| 25 |
+
return output_argv
|
| 26 |
+
|
| 27 |
+
def parse_args(parser, argv=None):
|
| 28 |
+
return parser.parse_args(extract_args(argv))
|
| 29 |
+
|
| 30 |
+
def delete_object(obj):
|
| 31 |
+
if not INSIDE_BLENDER:
|
| 32 |
+
return
|
| 33 |
+
bpy.ops.object.select_all(action='DESELECT')
|
| 34 |
+
obj.select_set(True)
|
| 35 |
+
bpy.context.view_layer.objects.active = obj
|
| 36 |
+
bpy.ops.object.delete()
|
| 37 |
+
|
| 38 |
+
def get_camera_coords(cam, pos):
|
| 39 |
+
if not INSIDE_BLENDER:
|
| 40 |
+
return (0, 0, 0)
|
| 41 |
+
scene = bpy.context.scene
|
| 42 |
+
x, y, z = bpy_extras.object_utils.world_to_camera_view(scene, cam, pos)
|
| 43 |
+
scale = scene.render.resolution_percentage / 100.0
|
| 44 |
+
w = int(scale * scene.render.resolution_x)
|
| 45 |
+
h = int(scale * scene.render.resolution_y)
|
| 46 |
+
px = int(round(x * w))
|
| 47 |
+
py = int(round(h - y * h))
|
| 48 |
+
return (px, py, z)
|
| 49 |
+
|
| 50 |
+
def set_layer(obj, layer_idx):
|
| 51 |
+
if not INSIDE_BLENDER:
|
| 52 |
+
return
|
| 53 |
+
obj.layers[layer_idx] = True
|
| 54 |
+
for i in range(len(obj.layers)):
|
| 55 |
+
obj.layers[i] = (i == layer_idx)
|
| 56 |
+
|
| 57 |
+
def add_object(object_dir, name, scale, loc, theta=0):
|
| 58 |
+
if not INSIDE_BLENDER:
|
| 59 |
+
return
|
| 60 |
+
object_dir = os.path.abspath(os.path.normpath(object_dir))
|
| 61 |
+
if not os.path.exists(object_dir):
|
| 62 |
+
print(f"ERROR: Object directory does not exist: {object_dir}")
|
| 63 |
+
return
|
| 64 |
+
count = 0
|
| 65 |
+
for obj in bpy.data.objects:
|
| 66 |
+
if obj.name.startswith(name):
|
| 67 |
+
count += 1
|
| 68 |
+
|
| 69 |
+
blend_file = os.path.join(object_dir, '%s.blend' % name)
|
| 70 |
+
blend_file = os.path.abspath(blend_file).replace('\\', '/')
|
| 71 |
+
if not os.path.exists(blend_file):
|
| 72 |
+
print(f"ERROR: Blend file does not exist: {blend_file}")
|
| 73 |
+
return
|
| 74 |
+
directory = blend_file + '/Object/'
|
| 75 |
+
try:
|
| 76 |
+
bpy.ops.wm.append(
|
| 77 |
+
directory=directory,
|
| 78 |
+
filename=name,
|
| 79 |
+
filter_blender=True
|
| 80 |
+
)
|
| 81 |
+
except Exception as e:
|
| 82 |
+
error_msg = str(e)
|
| 83 |
+
print(f"ERROR: Failed to load object {name} from {directory}: {error_msg}")
|
| 84 |
+
print(f" Error type: {type(e).__name__}")
|
| 85 |
+
raise
|
| 86 |
+
|
| 87 |
+
new_name = '%s_%d' % (name, count)
|
| 88 |
+
bpy.data.objects[name].name = new_name
|
| 89 |
+
|
| 90 |
+
x, y = loc
|
| 91 |
+
bpy.context.view_layer.objects.active = bpy.data.objects[new_name]
|
| 92 |
+
bpy.context.object.rotation_euler[2] = theta
|
| 93 |
+
bpy.ops.transform.resize(value=(scale, scale, scale))
|
| 94 |
+
bpy.ops.transform.translate(value=(x, y, scale))
|
| 95 |
+
|
| 96 |
+
def load_materials(material_dir):
|
| 97 |
+
if not INSIDE_BLENDER:
|
| 98 |
+
return
|
| 99 |
+
material_dir = os.path.abspath(os.path.normpath(material_dir))
|
| 100 |
+
if not os.path.exists(material_dir):
|
| 101 |
+
print(f"ERROR: Material directory does not exist: {material_dir}")
|
| 102 |
+
return
|
| 103 |
+
for fn in os.listdir(material_dir):
|
| 104 |
+
if not fn.endswith('.blend'): continue
|
| 105 |
+
name = os.path.splitext(fn)[0]
|
| 106 |
+
blend_file = os.path.join(material_dir, fn)
|
| 107 |
+
blend_file = os.path.abspath(blend_file).replace('\\', '/')
|
| 108 |
+
if not os.path.exists(blend_file):
|
| 109 |
+
print(f"ERROR: Blend file does not exist: {blend_file}")
|
| 110 |
+
continue
|
| 111 |
+
directory = blend_file + '/NodeTree/'
|
| 112 |
+
try:
|
| 113 |
+
bpy.ops.wm.append(
|
| 114 |
+
directory=directory,
|
| 115 |
+
filename=name,
|
| 116 |
+
filter_blender=True
|
| 117 |
+
)
|
| 118 |
+
except Exception as e:
|
| 119 |
+
error_msg = str(e)
|
| 120 |
+
print(f"ERROR: Failed to load material {name} from {directory}: {error_msg}")
|
| 121 |
+
print(f" Error type: {type(e).__name__}")
|
| 122 |
+
raise
|
| 123 |
+
|
| 124 |
+
def apply_filter_to_image(image_path, filter_type, filter_strength):
|
| 125 |
+
image_path = os.path.abspath(image_path)
|
| 126 |
+
if Image is None:
|
| 127 |
+
print(f"ERROR: PIL/Image not available, cannot apply filter {filter_type}")
|
| 128 |
+
return
|
| 129 |
+
if not os.path.exists(image_path):
|
| 130 |
+
print(f"ERROR: Image file does not exist: {image_path}")
|
| 131 |
+
return
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
img = Image.open(image_path)
|
| 135 |
+
|
| 136 |
+
if filter_type == 'blur':
|
| 137 |
+
radius = max(1, int(filter_strength))
|
| 138 |
+
img = img.filter(ImageFilter.GaussianBlur(radius=radius))
|
| 139 |
+
|
| 140 |
+
elif filter_type == 'vignette':
|
| 141 |
+
width, height = img.size
|
| 142 |
+
center_x, center_y = width // 2, height // 2
|
| 143 |
+
max_dist = math.sqrt(center_x**2 + center_y**2)
|
| 144 |
+
|
| 145 |
+
img = img.convert('RGB')
|
| 146 |
+
pixels = img.load()
|
| 147 |
+
for y in range(height):
|
| 148 |
+
for x in range(width):
|
| 149 |
+
dist = math.sqrt((x - center_x)**2 + (y - center_y)**2)
|
| 150 |
+
factor = 1.0 - (dist / max_dist) * (filter_strength / 5.0)
|
| 151 |
+
factor = max(0.0, min(1.0, factor))
|
| 152 |
+
|
| 153 |
+
r, g, b = pixels[x, y]
|
| 154 |
+
pixels[x, y] = (int(r * factor), int(g * factor), int(b * factor))
|
| 155 |
+
|
| 156 |
+
elif filter_type == 'fisheye':
|
| 157 |
+
width, height = img.size
|
| 158 |
+
center_x, center_y = width / 2.0, height / 2.0
|
| 159 |
+
max_radius = min(center_x, center_y)
|
| 160 |
+
|
| 161 |
+
img = img.convert('RGB')
|
| 162 |
+
output = Image.new('RGB', (width, height))
|
| 163 |
+
out_pixels = output.load()
|
| 164 |
+
in_pixels = img.load()
|
| 165 |
+
|
| 166 |
+
for y in range(height):
|
| 167 |
+
for x in range(width):
|
| 168 |
+
dx = (x - center_x) / max_radius
|
| 169 |
+
dy = (y - center_y) / max_radius
|
| 170 |
+
distance = math.sqrt(dx*dx + dy*dy)
|
| 171 |
+
|
| 172 |
+
if distance > 1.0:
|
| 173 |
+
out_pixels[x, y] = (0, 0, 0)
|
| 174 |
+
else:
|
| 175 |
+
theta = math.atan2(dy, dx)
|
| 176 |
+
r_normalized = distance
|
| 177 |
+
r_distorted = r_normalized * (1.0 + filter_strength * (1.0 - r_normalized))
|
| 178 |
+
r_distorted = min(1.0, r_distorted)
|
| 179 |
+
|
| 180 |
+
src_x = int(center_x + r_distorted * max_radius * math.cos(theta))
|
| 181 |
+
src_y = int(center_y + r_distorted * max_radius * math.sin(theta))
|
| 182 |
+
|
| 183 |
+
if 0 <= src_x < width and 0 <= src_y < height:
|
| 184 |
+
out_pixels[x, y] = in_pixels[src_x, src_y]
|
| 185 |
+
else:
|
| 186 |
+
out_pixels[x, y] = (0, 0, 0)
|
| 187 |
+
|
| 188 |
+
img = output
|
| 189 |
+
|
| 190 |
+
img.save(image_path)
|
| 191 |
+
print(f"[OK] Applied {filter_type} filter (strength: {filter_strength:.2f})")
|
| 192 |
+
except Exception as e:
|
| 193 |
+
import traceback
|
| 194 |
+
print(f"ERROR applying filter {filter_type}: {e}")
|
| 195 |
+
traceback.print_exc()
|
| 196 |
+
raise
|
| 197 |
+
|
| 198 |
+
def add_material(name, **properties):
|
| 199 |
+
if not INSIDE_BLENDER:
|
| 200 |
+
return
|
| 201 |
+
mat_count = len(bpy.data.materials)
|
| 202 |
+
bpy.ops.material.new()
|
| 203 |
+
mat = bpy.data.materials['Material']
|
| 204 |
+
mat.name = 'Material_%d' % mat_count
|
| 205 |
+
obj = bpy.context.active_object
|
| 206 |
+
assert len(obj.data.materials) == 0
|
| 207 |
+
obj.data.materials.append(mat)
|
| 208 |
+
|
| 209 |
+
output_node = None
|
| 210 |
+
for n in mat.node_tree.nodes:
|
| 211 |
+
if n.name == 'Material Output':
|
| 212 |
+
output_node = n
|
| 213 |
+
break
|
| 214 |
+
|
| 215 |
+
group_node = mat.node_tree.nodes.new('ShaderNodeGroup')
|
| 216 |
+
group_node.node_tree = bpy.data.node_groups[name]
|
| 217 |
+
|
| 218 |
+
for inp in group_node.inputs:
|
| 219 |
+
if inp.name in properties:
|
| 220 |
+
inp.default_value = properties[inp.name]
|
| 221 |
+
|
| 222 |
+
mat.node_tree.links.new(
|
| 223 |
+
group_node.outputs['Shader'],
|
| 224 |
+
output_node.inputs['Surface'],
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
parser = argparse.ArgumentParser()
|
| 228 |
+
parser.add_argument('--scene_file', default=None,
|
| 229 |
+
help="Optional JSON file to load scene from. If provided, renders from JSON instead of generating random scenes.")
|
| 230 |
+
parser.add_argument('--base_scene_blendfile', default='data/base_scene.blend',
|
| 231 |
+
help="Base blender file on which all scenes are based; includes " +
|
| 232 |
+
"ground plane, lights, and camera.")
|
| 233 |
+
parser.add_argument('--properties_json', default='data/properties.json',
|
| 234 |
+
help="JSON file defining objects, materials, sizes, and colors. " +
|
| 235 |
+
"The \"colors\" field maps from CLEVR color names to RGB values; " +
|
| 236 |
+
"The \"sizes\" field maps from CLEVR size names to scalars used to " +
|
| 237 |
+
"rescale object models; the \"materials\" and \"shapes\" fields map " +
|
| 238 |
+
"from CLEVR material and shape names to .blend files in the " +
|
| 239 |
+
"--object_material_dir and --shape_dir directories respectively.")
|
| 240 |
+
parser.add_argument('--shape_dir', default='data/shapes',
|
| 241 |
+
help="Directory where .blend files for object models are stored")
|
| 242 |
+
parser.add_argument('--material_dir', default='data/materials',
|
| 243 |
+
help="Directory where .blend files for materials are stored")
|
| 244 |
+
parser.add_argument('--shape_color_combos_json', default=None,
|
| 245 |
+
help="Optional path to a JSON file mapping shape names to a list of " +
|
| 246 |
+
"allowed color names for that shape. This allows rendering images " +
|
| 247 |
+
"for CLEVR-CoGenT.")
|
| 248 |
+
|
| 249 |
+
parser.add_argument('--min_objects', default=3, type=int,
|
| 250 |
+
help="The minimum number of objects to place in each scene")
|
| 251 |
+
parser.add_argument('--max_objects', default=10, type=int,
|
| 252 |
+
help="The maximum number of objects to place in each scene")
|
| 253 |
+
parser.add_argument('--min_dist', default=0.15, type=float,
|
| 254 |
+
help="The minimum allowed distance between object centers")
|
| 255 |
+
parser.add_argument('--margin', default=0.2, type=float,
|
| 256 |
+
help="Along all cardinal directions (left, right, front, back), all " +
|
| 257 |
+
"objects will be at least this distance apart. This makes resolving " +
|
| 258 |
+
"spatial relationships slightly less ambiguous.")
|
| 259 |
+
parser.add_argument('--min_pixels_per_object', default=50, type=int,
|
| 260 |
+
help="All objects will have at least this many visible pixels in the " +
|
| 261 |
+
"final rendered images; this ensures that no objects are fully " +
|
| 262 |
+
"occluded by other objects.")
|
| 263 |
+
parser.add_argument('--max_retries', default=100, type=int,
|
| 264 |
+
help="The number of times to try placing an object before giving up and " +
|
| 265 |
+
"re-placing all objects in the scene.")
|
| 266 |
+
|
| 267 |
+
parser.add_argument('--start_idx', default=0, type=int,
|
| 268 |
+
help="The index at which to start for numbering rendered images. Setting " +
|
| 269 |
+
"this to non-zero values allows you to distribute rendering across " +
|
| 270 |
+
"multiple machines and recombine the results later.")
|
| 271 |
+
parser.add_argument('--num_images', default=5, type=int,
|
| 272 |
+
help="The number of images to render")
|
| 273 |
+
parser.add_argument('--filename_prefix', default='CLEVR',
|
| 274 |
+
help="This prefix will be prepended to the rendered images and JSON scenes")
|
| 275 |
+
parser.add_argument('--split', default='new',
|
| 276 |
+
help="Name of the split for which we are rendering. This will be added to " +
|
| 277 |
+
"the names of rendered images, and will also be stored in the JSON " +
|
| 278 |
+
"scene structure for each image.")
|
| 279 |
+
parser.add_argument('--output_image_dir', default='../output/images/',
|
| 280 |
+
help="The directory where output images will be stored. It will be " +
|
| 281 |
+
"created if it does not exist.")
|
| 282 |
+
parser.add_argument('--output_scene_dir', default='../output/scenes/',
|
| 283 |
+
help="The directory where output JSON scene structures will be stored. " +
|
| 284 |
+
"It will be created if it does not exist.")
|
| 285 |
+
parser.add_argument('--output_scene_file', default='../output/CLEVR_scenes.json',
|
| 286 |
+
help="Path to write a single JSON file containing all scene information")
|
| 287 |
+
parser.add_argument('--output_blend_dir', default='output/blendfiles',
|
| 288 |
+
help="The directory where blender scene files will be stored, if the " +
|
| 289 |
+
"user requested that these files be saved using the " +
|
| 290 |
+
"--save_blendfiles flag; in this case it will be created if it does " +
|
| 291 |
+
"not already exist.")
|
| 292 |
+
parser.add_argument('--save_blendfiles', type=int, default=0,
|
| 293 |
+
help="Setting --save_blendfiles 1 will cause the blender scene file for " +
|
| 294 |
+
"each generated image to be stored in the directory specified by " +
|
| 295 |
+
"the --output_blend_dir flag. These files are not saved by default " +
|
| 296 |
+
"because they take up ~5-10MB each.")
|
| 297 |
+
parser.add_argument('--version', default='1.0',
|
| 298 |
+
help="String to store in the \"version\" field of the generated JSON file")
|
| 299 |
+
parser.add_argument('--license',
|
| 300 |
+
default="Creative Commons Attribution (CC-BY 4.0)",
|
| 301 |
+
help="String to store in the \"license\" field of the generated JSON file")
|
| 302 |
+
parser.add_argument('--date', default=dt.today().strftime("%m/%d/%Y"),
|
| 303 |
+
help="String to store in the \"date\" field of the generated JSON file; " +
|
| 304 |
+
"defaults to today's date")
|
| 305 |
+
|
| 306 |
+
parser.add_argument('--use_gpu', default=0, type=int,
|
| 307 |
+
help="Setting --use_gpu 1 enables GPU-accelerated rendering using CUDA. " +
|
| 308 |
+
"You must have an NVIDIA GPU with the CUDA toolkit installed for " +
|
| 309 |
+
"to work.")
|
| 310 |
+
parser.add_argument('--width', default=320, type=int,
|
| 311 |
+
help="The width (in pixels) for the rendered images")
|
| 312 |
+
parser.add_argument('--height', default=240, type=int,
|
| 313 |
+
help="The height (in pixels) for the rendered images")
|
| 314 |
+
parser.add_argument('--key_light_jitter', default=1.0, type=float,
|
| 315 |
+
help="The magnitude of random jitter to add to the key light position.")
|
| 316 |
+
parser.add_argument('--fill_light_jitter', default=1.0, type=float,
|
| 317 |
+
help="The magnitude of random jitter to add to the fill light position.")
|
| 318 |
+
parser.add_argument('--back_light_jitter', default=1.0, type=float,
|
| 319 |
+
help="The magnitude of random jitter to add to the back light position.")
|
| 320 |
+
parser.add_argument('--camera_jitter', default=0.5, type=float,
|
| 321 |
+
help="The magnitude of random jitter to add to the camera position")
|
| 322 |
+
parser.add_argument('--render_num_samples', default=512, type=int,
|
| 323 |
+
help="The number of samples to use when rendering. Larger values will " +
|
| 324 |
+
"result in nicer images but will cause rendering to take longer.")
|
| 325 |
+
parser.add_argument('--render_min_bounces', default=8, type=int,
|
| 326 |
+
help="The minimum number of bounces to use for rendering.")
|
| 327 |
+
parser.add_argument('--render_max_bounces', default=8, type=int,
|
| 328 |
+
help="The maximum number of bounces to use for rendering.")
|
| 329 |
+
parser.add_argument('--render_tile_size', default=256, type=int,
|
| 330 |
+
help="The tile size to use for rendering. This should not affect the " +
|
| 331 |
+
"quality of the rendered image but may affect the speed; CPU-based " +
|
| 332 |
+
"rendering may achieve better performance using smaller tile sizes " +
|
| 333 |
+
"while larger tile sizes may be optimal for GPU-based rendering.")
|
| 334 |
+
parser.add_argument('--output_image', default=None,
|
| 335 |
+
help="Output image path (used when rendering from JSON)")
|
| 336 |
+
|
| 337 |
+
MIN_VISIBLE_FRACTION = 0.001
|
| 338 |
+
MIN_VISIBLE_FRACTION_PARTIAL_OCCLUSION = 0.0005
|
| 339 |
+
MIN_PIXELS_FLOOR = 50
|
| 340 |
+
|
| 341 |
+
BASE_MIN_VISIBILITY_FRACTION = 0.9
|
| 342 |
+
CF_OCCLUSION_MIN_VISIBILITY_FRACTION = 0.3
|
| 343 |
+
CF_OCCLUSION_MAX_VISIBILITY_FRACTION = 0.5
|
| 344 |
+
CF_OCCLUSION_HARD_MIN_FRACTION = 0.2
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
def min_visible_pixels(width, height, fraction=MIN_VISIBLE_FRACTION, floor=MIN_PIXELS_FLOOR):
|
| 348 |
+
return max(floor, int(width * height * fraction))
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
BACKGROUND_COLORS = {
|
| 352 |
+
'default': None,
|
| 353 |
+
'gray': (0.5, 0.5, 0.5),
|
| 354 |
+
'blue': (0.2, 0.4, 0.8),
|
| 355 |
+
'green': (0.2, 0.6, 0.3),
|
| 356 |
+
'brown': (0.4, 0.3, 0.2),
|
| 357 |
+
'purple': (0.5, 0.3, 0.6),
|
| 358 |
+
'orange': (0.8, 0.5, 0.2),
|
| 359 |
+
'white': (0.9, 0.9, 0.9),
|
| 360 |
+
'dark_gray': (0.2, 0.2, 0.2),
|
| 361 |
+
'red': (0.7, 0.2, 0.2),
|
| 362 |
+
'yellow': (0.8, 0.8, 0.3),
|
| 363 |
+
'cyan': (0.3, 0.7, 0.8),
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
LIGHTING_PRESETS = {
|
| 367 |
+
'default': {'key': 1.0, 'fill': 0.5, 'back': 0.3},
|
| 368 |
+
'bright': {'key': 12.0, 'fill': 6.0, 'back': 4.0},
|
| 369 |
+
'dim': {'key': 0.008, 'fill': 0.004, 'back': 0.002},
|
| 370 |
+
'warm': {'key': 5.0, 'fill': 0.8, 'back': 0.3, 'color': (1.0, 0.5, 0.2)},
|
| 371 |
+
'cool': {'key': 4.0, 'fill': 2.0, 'back': 1.5, 'color': (0.2, 0.5, 1.0)},
|
| 372 |
+
'dramatic': {'key': 15.0, 'fill': 0.005, 'back': 0.002},
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
def set_background_color(color_name):
|
| 376 |
+
"""Set the world background color"""
|
| 377 |
+
if not INSIDE_BLENDER:
|
| 378 |
+
return
|
| 379 |
+
if color_name not in BACKGROUND_COLORS or BACKGROUND_COLORS[color_name] is None:
|
| 380 |
+
return
|
| 381 |
+
|
| 382 |
+
rgb = BACKGROUND_COLORS[color_name]
|
| 383 |
+
|
| 384 |
+
world = bpy.context.scene.world
|
| 385 |
+
if world is None:
|
| 386 |
+
world = bpy.data.worlds.new("World")
|
| 387 |
+
bpy.context.scene.world = world
|
| 388 |
+
|
| 389 |
+
world.use_nodes = True
|
| 390 |
+
nodes = world.node_tree.nodes
|
| 391 |
+
|
| 392 |
+
bg_node = None
|
| 393 |
+
for node in nodes:
|
| 394 |
+
if node.type == 'BACKGROUND':
|
| 395 |
+
bg_node = node
|
| 396 |
+
break
|
| 397 |
+
|
| 398 |
+
if bg_node is None:
|
| 399 |
+
bg_node = nodes.new(type='ShaderNodeBackground')
|
| 400 |
+
|
| 401 |
+
bg_node.inputs['Color'].default_value = (rgb[0], rgb[1], rgb[2], 1.0)
|
| 402 |
+
bg_node.inputs['Strength'].default_value = 1.0
|
| 403 |
+
|
| 404 |
+
print(f"Set background color to {color_name}: RGB{rgb}")
|
| 405 |
+
|
| 406 |
+
def set_ground_color(color_name):
|
| 407 |
+
if not INSIDE_BLENDER:
|
| 408 |
+
return
|
| 409 |
+
if color_name not in BACKGROUND_COLORS or BACKGROUND_COLORS[color_name] is None:
|
| 410 |
+
return
|
| 411 |
+
|
| 412 |
+
rgb = BACKGROUND_COLORS[color_name]
|
| 413 |
+
|
| 414 |
+
ground = None
|
| 415 |
+
for obj in bpy.data.objects:
|
| 416 |
+
if 'ground' in obj.name.lower() or 'plane' in obj.name.lower():
|
| 417 |
+
ground = obj
|
| 418 |
+
break
|
| 419 |
+
|
| 420 |
+
if ground is None:
|
| 421 |
+
return
|
| 422 |
+
|
| 423 |
+
if len(ground.data.materials) == 0:
|
| 424 |
+
mat = bpy.data.materials.new(name="Ground_Material")
|
| 425 |
+
ground.data.materials.append(mat)
|
| 426 |
+
else:
|
| 427 |
+
mat = ground.data.materials[0]
|
| 428 |
+
|
| 429 |
+
mat.use_nodes = True
|
| 430 |
+
nodes = mat.node_tree.nodes
|
| 431 |
+
|
| 432 |
+
bsdf = None
|
| 433 |
+
for node in nodes:
|
| 434 |
+
if node.type == 'BSDF_PRINCIPLED':
|
| 435 |
+
bsdf = node
|
| 436 |
+
break
|
| 437 |
+
|
| 438 |
+
if bsdf:
|
| 439 |
+
bsdf.inputs['Base Color'].default_value = (rgb[0], rgb[1], rgb[2], 1.0)
|
| 440 |
+
print(f"Set ground color to {color_name}")
|
| 441 |
+
|
| 442 |
+
def set_lighting(lighting_name):
|
| 443 |
+
"""Set lighting conditions"""
|
| 444 |
+
if not INSIDE_BLENDER:
|
| 445 |
+
return
|
| 446 |
+
if lighting_name not in LIGHTING_PRESETS:
|
| 447 |
+
return
|
| 448 |
+
|
| 449 |
+
preset = LIGHTING_PRESETS[lighting_name]
|
| 450 |
+
|
| 451 |
+
lamp_names = ['Lamp_Key', 'Lamp_Fill', 'Lamp_Back']
|
| 452 |
+
intensity_keys = ['key', 'fill', 'back']
|
| 453 |
+
|
| 454 |
+
for lamp_name, int_key in zip(lamp_names, intensity_keys):
|
| 455 |
+
if lamp_name in bpy.data.objects:
|
| 456 |
+
lamp_obj = bpy.data.objects[lamp_name]
|
| 457 |
+
if lamp_obj.data and hasattr(lamp_obj.data, 'energy'):
|
| 458 |
+
base_energy = lamp_obj.data.energy
|
| 459 |
+
lamp_obj.data.energy = base_energy * preset.get(int_key, 1.0)
|
| 460 |
+
|
| 461 |
+
if 'color' in preset and hasattr(lamp_obj.data, 'color'):
|
| 462 |
+
lamp_obj.data.color = preset['color']
|
| 463 |
+
|
| 464 |
+
print(f"Set lighting to {lighting_name}")
|
| 465 |
+
|
| 466 |
+
def render_from_json(args):
|
| 467 |
+
if not INSIDE_BLENDER:
|
| 468 |
+
print("ERROR: render_from_json must be run inside Blender")
|
| 469 |
+
return
|
| 470 |
+
|
| 471 |
+
output_dir = os.path.dirname(args.output_image) if args.output_image else '.'
|
| 472 |
+
if output_dir and not os.path.exists(output_dir):
|
| 473 |
+
os.makedirs(output_dir)
|
| 474 |
+
|
| 475 |
+
with open(args.scene_file, 'r') as f:
|
| 476 |
+
scene_struct = json.load(f)
|
| 477 |
+
|
| 478 |
+
num_objects = len(scene_struct.get('objects', []))
|
| 479 |
+
print(f"Scene has {num_objects} objects")
|
| 480 |
+
|
| 481 |
+
base_scene_path = os.path.abspath(args.base_scene_blendfile)
|
| 482 |
+
bpy.ops.wm.open_mainfile(filepath=base_scene_path)
|
| 483 |
+
|
| 484 |
+
try:
|
| 485 |
+
load_materials(args.material_dir)
|
| 486 |
+
except Exception as e:
|
| 487 |
+
print(f"Warning: Could not load materials: {e}")
|
| 488 |
+
|
| 489 |
+
background_color = scene_struct.get('background_color', None)
|
| 490 |
+
if background_color:
|
| 491 |
+
set_background_color(background_color)
|
| 492 |
+
set_ground_color(background_color)
|
| 493 |
+
|
| 494 |
+
lighting = scene_struct.get('lighting', None)
|
| 495 |
+
if lighting:
|
| 496 |
+
set_lighting(lighting)
|
| 497 |
+
|
| 498 |
+
render_args = bpy.context.scene.render
|
| 499 |
+
render_args.engine = "CYCLES"
|
| 500 |
+
render_args.filepath = args.output_image
|
| 501 |
+
render_args.resolution_x = args.width
|
| 502 |
+
render_args.resolution_y = args.height
|
| 503 |
+
render_args.resolution_percentage = 100
|
| 504 |
+
|
| 505 |
+
if args.use_gpu == 1:
|
| 506 |
+
try:
|
| 507 |
+
bpy.context.preferences.addons['cycles'].preferences.compute_device_type = 'CUDA'
|
| 508 |
+
bpy.context.scene.cycles.device = 'GPU'
|
| 509 |
+
print("[OK] GPU rendering enabled")
|
| 510 |
+
except Exception as e:
|
| 511 |
+
print(f"Warning: Could not enable GPU: {e}")
|
| 512 |
+
|
| 513 |
+
bpy.context.scene.cycles.samples = args.render_num_samples
|
| 514 |
+
|
| 515 |
+
filter_type = scene_struct.get('filter_type')
|
| 516 |
+
filter_strength = scene_struct.get('filter_strength', 1.0)
|
| 517 |
+
|
| 518 |
+
if filter_type == 'fisheye':
|
| 519 |
+
camera = bpy.data.objects.get('Camera')
|
| 520 |
+
if camera and camera.data:
|
| 521 |
+
cam_data = camera.data
|
| 522 |
+
if cam_data.type == 'PERSP':
|
| 523 |
+
cam_data.lens = cam_data.lens * 0.7
|
| 524 |
+
print(f"[OK] Zoomed out camera for fisheye: lens={cam_data.lens:.1f}mm")
|
| 525 |
+
|
| 526 |
+
with open(args.properties_json, 'r') as f:
|
| 527 |
+
properties = json.load(f)
|
| 528 |
+
color_name_to_rgba = {}
|
| 529 |
+
for name, rgb in properties['colors'].items():
|
| 530 |
+
rgba = [float(c) / 255.0 for c in rgb] + [1.0]
|
| 531 |
+
color_name_to_rgba[name] = rgba
|
| 532 |
+
size_mapping = properties['sizes']
|
| 533 |
+
|
| 534 |
+
shape_semantic_to_file = properties['shapes']
|
| 535 |
+
material_semantic_to_file = properties['materials']
|
| 536 |
+
|
| 537 |
+
blender_objects = []
|
| 538 |
+
print("Adding objects to scene...")
|
| 539 |
+
for i, obj_info in enumerate(scene_struct.get('objects', [])):
|
| 540 |
+
x, y, z = obj_info['3d_coords']
|
| 541 |
+
r = size_mapping[obj_info['size']]
|
| 542 |
+
semantic_shape = obj_info['shape']
|
| 543 |
+
|
| 544 |
+
if semantic_shape == 'cube':
|
| 545 |
+
r /= math.sqrt(2)
|
| 546 |
+
|
| 547 |
+
if semantic_shape not in shape_semantic_to_file:
|
| 548 |
+
print(f"ERROR: Shape '{semantic_shape}' not found")
|
| 549 |
+
continue
|
| 550 |
+
|
| 551 |
+
shape_file_name = shape_semantic_to_file[semantic_shape]
|
| 552 |
+
|
| 553 |
+
try:
|
| 554 |
+
add_object(args.shape_dir, shape_file_name, r, (x, y), theta=obj_info['rotation'])
|
| 555 |
+
except Exception as e:
|
| 556 |
+
print(f"Error adding object {i}: {e}")
|
| 557 |
+
continue
|
| 558 |
+
if INSIDE_BLENDER and bpy.context.object:
|
| 559 |
+
blender_objects.append(bpy.context.object)
|
| 560 |
+
|
| 561 |
+
rgba = color_name_to_rgba[obj_info['color']]
|
| 562 |
+
semantic_material = obj_info['material']
|
| 563 |
+
|
| 564 |
+
if semantic_material not in material_semantic_to_file:
|
| 565 |
+
print(f"ERROR: Material '{semantic_material}' not found")
|
| 566 |
+
continue
|
| 567 |
+
|
| 568 |
+
mat_file_name = material_semantic_to_file[semantic_material]
|
| 569 |
+
|
| 570 |
+
try:
|
| 571 |
+
add_material(mat_file_name, Color=rgba)
|
| 572 |
+
except Exception as e:
|
| 573 |
+
print(f"Warning: Could not add material: {e}")
|
| 574 |
+
|
| 575 |
+
if blender_objects:
|
| 576 |
+
cf_meta = scene_struct.get('cf_metadata') or {}
|
| 577 |
+
cf_type = cf_meta.get('cf_type', '')
|
| 578 |
+
|
| 579 |
+
visibility_info = None
|
| 580 |
+
if INSIDE_BLENDER and Image is not None:
|
| 581 |
+
try:
|
| 582 |
+
visibility_info = compute_visibility_fractions(blender_objects)
|
| 583 |
+
except Exception as e:
|
| 584 |
+
print(f"Warning: compute_visibility_fractions failed during render: {e}")
|
| 585 |
+
visibility_info = None
|
| 586 |
+
|
| 587 |
+
all_visible = True
|
| 588 |
+
fail_reason = 'unknown visibility failure'
|
| 589 |
+
|
| 590 |
+
if visibility_info is not None:
|
| 591 |
+
ratios, scene_counts, full_counts = visibility_info
|
| 592 |
+
|
| 593 |
+
if cf_type == 'occlusion_change':
|
| 594 |
+
too_hidden = [
|
| 595 |
+
(i, r) for i, r in enumerate(ratios)
|
| 596 |
+
if full_counts[i] > 0 and r < CF_OCCLUSION_HARD_MIN_FRACTION
|
| 597 |
+
]
|
| 598 |
+
band_objects = [
|
| 599 |
+
(i, r) for i, r in enumerate(ratios)
|
| 600 |
+
if full_counts[i] > 0 and CF_OCCLUSION_MIN_VISIBILITY_FRACTION <= r <= CF_OCCLUSION_MAX_VISIBILITY_FRACTION
|
| 601 |
+
]
|
| 602 |
+
|
| 603 |
+
if too_hidden:
|
| 604 |
+
all_visible = False
|
| 605 |
+
min_r = min(r for (_, r) in too_hidden)
|
| 606 |
+
fail_reason = (f'at least one object is too occluded in occlusion_change CF; '
|
| 607 |
+
f'min visibility fraction={min_r:.3f} '
|
| 608 |
+
f'(required >= {CF_OCCLUSION_HARD_MIN_FRACTION})')
|
| 609 |
+
elif not band_objects:
|
| 610 |
+
all_visible = False
|
| 611 |
+
fail_reason = (f'no object falls into required occlusion band '
|
| 612 |
+
f'[{CF_OCCLUSION_MIN_VISIBILITY_FRACTION}, '
|
| 613 |
+
f'{CF_OCCLUSION_MAX_VISIBILITY_FRACTION}]')
|
| 614 |
+
else:
|
| 615 |
+
all_visible = True
|
| 616 |
+
|
| 617 |
+
else:
|
| 618 |
+
too_occluded = [
|
| 619 |
+
(i, r) for i, r in enumerate(ratios)
|
| 620 |
+
if full_counts[i] > 0 and r < BASE_MIN_VISIBILITY_FRACTION
|
| 621 |
+
]
|
| 622 |
+
if too_occluded:
|
| 623 |
+
all_visible = False
|
| 624 |
+
min_r = min(r for (_, r) in too_occluded)
|
| 625 |
+
fail_reason = (f'at least one object is too occluded in base scene; '
|
| 626 |
+
f'min visibility fraction={min_r:.3f} '
|
| 627 |
+
f'(required >= {BASE_MIN_VISIBILITY_FRACTION})')
|
| 628 |
+
else:
|
| 629 |
+
all_visible = True
|
| 630 |
+
|
| 631 |
+
else:
|
| 632 |
+
# Fallback to legacy absolute pixel-based visibility when we cannot
|
| 633 |
+
# compute per-object relative visibility (e.g., PIL not available).
|
| 634 |
+
w = getattr(args, 'width', 320)
|
| 635 |
+
h = getattr(args, 'height', 240)
|
| 636 |
+
if cf_type == 'occlusion_change':
|
| 637 |
+
min_pixels = min_visible_pixels(w, h, MIN_VISIBLE_FRACTION_PARTIAL_OCCLUSION, MIN_PIXELS_FLOOR)
|
| 638 |
+
else:
|
| 639 |
+
base = min_visible_pixels(w, h, MIN_VISIBLE_FRACTION, MIN_PIXELS_FLOOR)
|
| 640 |
+
min_pixels = max(getattr(args, 'min_pixels_per_object', MIN_PIXELS_FLOOR), base)
|
| 641 |
+
all_visible = check_visibility(blender_objects, min_pixels)
|
| 642 |
+
if not all_visible:
|
| 643 |
+
fail_reason = 'at least one object has too few visible pixels'
|
| 644 |
+
|
| 645 |
+
if not all_visible:
|
| 646 |
+
print(f'Visibility check failed: {fail_reason}')
|
| 647 |
+
for obj in blender_objects:
|
| 648 |
+
try:
|
| 649 |
+
delete_object(obj)
|
| 650 |
+
except Exception:
|
| 651 |
+
pass
|
| 652 |
+
sys.exit(1)
|
| 653 |
+
|
| 654 |
+
filter_type = scene_struct.get('filter_type')
|
| 655 |
+
filter_strength = scene_struct.get('filter_strength', 1.0)
|
| 656 |
+
|
| 657 |
+
print(f"Rendering to {args.output_image}...")
|
| 658 |
+
|
| 659 |
+
try:
|
| 660 |
+
bpy.ops.render.render(write_still=True)
|
| 661 |
+
print("[OK] Rendering complete!")
|
| 662 |
+
except Exception as e:
|
| 663 |
+
print(f"Error during rendering: {e}")
|
| 664 |
+
sys.exit(1)
|
| 665 |
+
|
| 666 |
+
post_filter_type = scene_struct.get('filter_type')
|
| 667 |
+
if post_filter_type and post_filter_type != 'fisheye':
|
| 668 |
+
if Image is None:
|
| 669 |
+
print(f"Warning: PIL not available, cannot apply post-filter {post_filter_type}")
|
| 670 |
+
elif not os.path.exists(args.output_image):
|
| 671 |
+
print(f"Warning: Output image does not exist: {args.output_image}")
|
| 672 |
+
else:
|
| 673 |
+
try:
|
| 674 |
+
post_filter_strength = scene_struct.get('filter_strength', 1.0)
|
| 675 |
+
apply_filter_to_image(args.output_image, post_filter_type, post_filter_strength)
|
| 676 |
+
except Exception as e:
|
| 677 |
+
import traceback
|
| 678 |
+
print(f"Warning: Failed to apply post-filter {post_filter_type}: {e}")
|
| 679 |
+
traceback.print_exc()
|
| 680 |
+
|
| 681 |
+
def main(args):
|
| 682 |
+
if args.scene_file:
|
| 683 |
+
render_from_json(args)
|
| 684 |
+
return
|
| 685 |
+
|
| 686 |
+
num_digits = 6
|
| 687 |
+
prefix = '%s_%s_' % (args.filename_prefix, args.split)
|
| 688 |
+
img_template = '%s%%0%dd.png' % (prefix, num_digits)
|
| 689 |
+
scene_template = '%s%%0%dd.json' % (prefix, num_digits)
|
| 690 |
+
blend_template = '%s%%0%dd.blend' % (prefix, num_digits)
|
| 691 |
+
img_template = os.path.join(args.output_image_dir, img_template)
|
| 692 |
+
scene_template = os.path.join(args.output_scene_dir, scene_template)
|
| 693 |
+
blend_template = os.path.join(args.output_blend_dir, blend_template)
|
| 694 |
+
|
| 695 |
+
if not os.path.isdir(args.output_image_dir):
|
| 696 |
+
os.makedirs(args.output_image_dir)
|
| 697 |
+
if not os.path.isdir(args.output_scene_dir):
|
| 698 |
+
os.makedirs(args.output_scene_dir)
|
| 699 |
+
if args.save_blendfiles == 1 and not os.path.isdir(args.output_blend_dir):
|
| 700 |
+
os.makedirs(args.output_blend_dir)
|
| 701 |
+
|
| 702 |
+
all_scene_paths = []
|
| 703 |
+
for i in range(args.num_images):
|
| 704 |
+
img_path = img_template % (i + args.start_idx)
|
| 705 |
+
scene_path = scene_template % (i + args.start_idx)
|
| 706 |
+
all_scene_paths.append(scene_path)
|
| 707 |
+
blend_path = None
|
| 708 |
+
if args.save_blendfiles == 1:
|
| 709 |
+
blend_path = blend_template % (i + args.start_idx)
|
| 710 |
+
num_objects = random.randint(args.min_objects, args.max_objects)
|
| 711 |
+
render_scene(args,
|
| 712 |
+
num_objects=num_objects,
|
| 713 |
+
output_index=(i + args.start_idx),
|
| 714 |
+
output_split=args.split,
|
| 715 |
+
output_image=img_path,
|
| 716 |
+
output_scene=scene_path,
|
| 717 |
+
output_blendfile=blend_path,
|
| 718 |
+
)
|
| 719 |
+
|
| 720 |
+
all_scenes = []
|
| 721 |
+
for scene_path in all_scene_paths:
|
| 722 |
+
with open(scene_path, 'r') as f:
|
| 723 |
+
all_scenes.append(json.load(f))
|
| 724 |
+
output = {
|
| 725 |
+
'info': {
|
| 726 |
+
'date': args.date,
|
| 727 |
+
'version': args.version,
|
| 728 |
+
'split': args.split,
|
| 729 |
+
'license': args.license,
|
| 730 |
+
},
|
| 731 |
+
'scenes': all_scenes
|
| 732 |
+
}
|
| 733 |
+
if args.output_scene_file:
|
| 734 |
+
output_dir = os.path.dirname(os.path.abspath(args.output_scene_file))
|
| 735 |
+
if output_dir:
|
| 736 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 737 |
+
with open(args.output_scene_file, 'w') as f:
|
| 738 |
+
json.dump(output, f)
|
| 739 |
+
|
| 740 |
+
|
| 741 |
+
|
| 742 |
+
def render_scene(args,
|
| 743 |
+
num_objects=5,
|
| 744 |
+
output_index=0,
|
| 745 |
+
output_split='none',
|
| 746 |
+
output_image='render.png',
|
| 747 |
+
output_scene='render_json',
|
| 748 |
+
output_blendfile=None,
|
| 749 |
+
):
|
| 750 |
+
|
| 751 |
+
base_scene_path = os.path.abspath(args.base_scene_blendfile)
|
| 752 |
+
bpy.ops.wm.open_mainfile(filepath=base_scene_path)
|
| 753 |
+
load_materials(args.material_dir)
|
| 754 |
+
|
| 755 |
+
render_args = bpy.context.scene.render
|
| 756 |
+
render_args.engine = "CYCLES"
|
| 757 |
+
render_args.filepath = output_image
|
| 758 |
+
render_args.resolution_x = args.width
|
| 759 |
+
render_args.resolution_y = args.height
|
| 760 |
+
render_args.resolution_percentage = 100
|
| 761 |
+
if args.use_gpu == 1:
|
| 762 |
+
bpy.context.preferences.addons['cycles'].preferences.compute_device_type = 'CUDA'
|
| 763 |
+
bpy.context.preferences.addons['cycles'].preferences.get_devices()
|
| 764 |
+
for device in bpy.context.preferences.addons['cycles'].preferences.devices:
|
| 765 |
+
device.use = True
|
| 766 |
+
|
| 767 |
+
bpy.data.worlds['World'].cycles.sample_as_light = True
|
| 768 |
+
bpy.context.scene.cycles.blur_glossy = 2.0
|
| 769 |
+
bpy.context.scene.cycles.samples = args.render_num_samples
|
| 770 |
+
bpy.context.scene.cycles.transparent_min_bounces = args.render_min_bounces
|
| 771 |
+
bpy.context.scene.cycles.transparent_max_bounces = args.render_max_bounces
|
| 772 |
+
if args.use_gpu == 1:
|
| 773 |
+
bpy.context.scene.cycles.device = 'GPU'
|
| 774 |
+
|
| 775 |
+
scene_struct = {
|
| 776 |
+
'split': output_split,
|
| 777 |
+
'image_index': output_index,
|
| 778 |
+
'image_filename': os.path.basename(output_image),
|
| 779 |
+
'objects': [],
|
| 780 |
+
'directions': {},
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
bpy.ops.mesh.primitive_plane_add(size=10, location=(0, 0, 0))
|
| 784 |
+
plane = bpy.context.object
|
| 785 |
+
|
| 786 |
+
def rand(L):
|
| 787 |
+
return 2.0 * L * (random.random() - 0.5)
|
| 788 |
+
|
| 789 |
+
if args.camera_jitter > 0:
|
| 790 |
+
for i in range(3):
|
| 791 |
+
bpy.data.objects['Camera'].location[i] += rand(args.camera_jitter)
|
| 792 |
+
|
| 793 |
+
camera = bpy.data.objects['Camera']
|
| 794 |
+
plane_normal = plane.data.vertices[0].normal
|
| 795 |
+
cam_behind = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
|
| 796 |
+
cam_left = camera.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
|
| 797 |
+
cam_up = camera.matrix_world.to_quaternion() @ Vector((0, 1, 0))
|
| 798 |
+
plane_behind = (cam_behind - cam_behind.project(plane_normal)).normalized()
|
| 799 |
+
plane_left = (cam_left - cam_left.project(plane_normal)).normalized()
|
| 800 |
+
plane_up = cam_up.project(plane_normal).normalized()
|
| 801 |
+
|
| 802 |
+
delete_object(plane)
|
| 803 |
+
|
| 804 |
+
scene_struct['directions']['behind'] = tuple(plane_behind)
|
| 805 |
+
scene_struct['directions']['front'] = tuple(-plane_behind)
|
| 806 |
+
scene_struct['directions']['left'] = tuple(plane_left)
|
| 807 |
+
scene_struct['directions']['right'] = tuple(-plane_left)
|
| 808 |
+
scene_struct['directions']['above'] = tuple(plane_up)
|
| 809 |
+
scene_struct['directions']['below'] = tuple(-plane_up)
|
| 810 |
+
|
| 811 |
+
if args.key_light_jitter > 0:
|
| 812 |
+
for i in range(3):
|
| 813 |
+
bpy.data.objects['Lamp_Key'].location[i] += rand(args.key_light_jitter)
|
| 814 |
+
if args.back_light_jitter > 0:
|
| 815 |
+
for i in range(3):
|
| 816 |
+
bpy.data.objects['Lamp_Back'].location[i] += rand(args.back_light_jitter)
|
| 817 |
+
if args.fill_light_jitter > 0:
|
| 818 |
+
for i in range(3):
|
| 819 |
+
bpy.data.objects['Lamp_Fill'].location[i] += rand(args.fill_light_jitter)
|
| 820 |
+
|
| 821 |
+
objects, blender_objects = add_random_objects(scene_struct, num_objects, args, camera)
|
| 822 |
+
scene_struct['objects'] = objects
|
| 823 |
+
scene_struct['relationships'] = compute_all_relationships(scene_struct)
|
| 824 |
+
while True:
|
| 825 |
+
try:
|
| 826 |
+
bpy.ops.render.render(write_still=True)
|
| 827 |
+
break
|
| 828 |
+
except Exception as e:
|
| 829 |
+
print(e)
|
| 830 |
+
|
| 831 |
+
with open(output_scene, 'w') as f:
|
| 832 |
+
json.dump(scene_struct, f, indent=2)
|
| 833 |
+
|
| 834 |
+
if output_blendfile is not None:
|
| 835 |
+
bpy.ops.wm.save_as_mainfile(filepath=output_blendfile)
|
| 836 |
+
|
| 837 |
+
|
| 838 |
+
def add_random_objects(scene_struct, num_objects, args, camera, max_scene_attempts=10):
|
| 839 |
+
scene_attempt = 0
|
| 840 |
+
while scene_attempt < max_scene_attempts:
|
| 841 |
+
scene_attempt += 1
|
| 842 |
+
|
| 843 |
+
with open(args.properties_json, 'r') as f:
|
| 844 |
+
properties = json.load(f)
|
| 845 |
+
color_name_to_rgba = {}
|
| 846 |
+
for name, rgb in properties['colors'].items():
|
| 847 |
+
rgba = [float(c) / 255.0 for c in rgb] + [1.0]
|
| 848 |
+
color_name_to_rgba[name] = rgba
|
| 849 |
+
material_mapping = [(v, k) for k, v in properties['materials'].items()]
|
| 850 |
+
object_mapping = [(v, k) for k, v in properties['shapes'].items()]
|
| 851 |
+
size_mapping = list(properties['sizes'].items())
|
| 852 |
+
|
| 853 |
+
shape_color_combos = None
|
| 854 |
+
if args.shape_color_combos_json is not None:
|
| 855 |
+
with open(args.shape_color_combos_json, 'r') as f:
|
| 856 |
+
shape_color_combos = list(json.load(f).items())
|
| 857 |
+
|
| 858 |
+
positions = []
|
| 859 |
+
objects = []
|
| 860 |
+
blender_objects = []
|
| 861 |
+
for i in range(num_objects):
|
| 862 |
+
size_name, r = random.choice(size_mapping)
|
| 863 |
+
|
| 864 |
+
num_tries = 0
|
| 865 |
+
while True:
|
| 866 |
+
num_tries += 1
|
| 867 |
+
if num_tries > args.max_retries:
|
| 868 |
+
for obj in blender_objects:
|
| 869 |
+
delete_object(obj)
|
| 870 |
+
break
|
| 871 |
+
x = random.uniform(-3, 3)
|
| 872 |
+
y = random.uniform(-3, 3)
|
| 873 |
+
dists_good = True
|
| 874 |
+
margins_good = True
|
| 875 |
+
for (xx, yy, rr) in positions:
|
| 876 |
+
dx, dy = x - xx, y - yy
|
| 877 |
+
dist = math.sqrt(dx * dx + dy * dy)
|
| 878 |
+
if dist - r - rr < args.min_dist:
|
| 879 |
+
dists_good = False
|
| 880 |
+
break
|
| 881 |
+
for direction_name in ['left', 'right', 'front', 'behind']:
|
| 882 |
+
direction_vec = scene_struct['directions'][direction_name]
|
| 883 |
+
assert direction_vec[2] == 0
|
| 884 |
+
margin = dx * direction_vec[0] + dy * direction_vec[1]
|
| 885 |
+
if 0 < margin < args.margin:
|
| 886 |
+
print(margin, args.margin, direction_name)
|
| 887 |
+
print('BROKEN MARGIN!')
|
| 888 |
+
margins_good = False
|
| 889 |
+
break
|
| 890 |
+
if not margins_good:
|
| 891 |
+
break
|
| 892 |
+
|
| 893 |
+
if dists_good and margins_good:
|
| 894 |
+
break
|
| 895 |
+
|
| 896 |
+
if num_tries > args.max_retries:
|
| 897 |
+
break
|
| 898 |
+
|
| 899 |
+
if shape_color_combos is None:
|
| 900 |
+
obj_name, obj_name_out = random.choice(object_mapping)
|
| 901 |
+
color_name, rgba = random.choice(list(color_name_to_rgba.items()))
|
| 902 |
+
else:
|
| 903 |
+
obj_name_out, color_choices = random.choice(shape_color_combos)
|
| 904 |
+
color_name = random.choice(color_choices)
|
| 905 |
+
obj_name = [k for k, v in object_mapping if v == obj_name_out][0]
|
| 906 |
+
rgba = color_name_to_rgba[color_name]
|
| 907 |
+
|
| 908 |
+
if obj_name == 'Cube':
|
| 909 |
+
r /= math.sqrt(2)
|
| 910 |
+
|
| 911 |
+
theta = 360.0 * random.random()
|
| 912 |
+
add_object(args.shape_dir, obj_name, r, (x, y), theta=theta)
|
| 913 |
+
obj = bpy.context.object
|
| 914 |
+
blender_objects.append(obj)
|
| 915 |
+
positions.append((x, y, r))
|
| 916 |
+
|
| 917 |
+
mat_name, mat_name_out = random.choice(material_mapping)
|
| 918 |
+
add_material(mat_name, Color=rgba)
|
| 919 |
+
|
| 920 |
+
pixel_coords = get_camera_coords(camera, obj.location)
|
| 921 |
+
objects.append({
|
| 922 |
+
'shape': obj_name_out,
|
| 923 |
+
'size': size_name,
|
| 924 |
+
'material': mat_name_out,
|
| 925 |
+
'3d_coords': tuple(obj.location),
|
| 926 |
+
'rotation': theta,
|
| 927 |
+
'pixel_coords': pixel_coords,
|
| 928 |
+
'color': color_name,
|
| 929 |
+
})
|
| 930 |
+
|
| 931 |
+
if len(objects) < num_objects:
|
| 932 |
+
continue
|
| 933 |
+
|
| 934 |
+
visibility_info = None
|
| 935 |
+
if INSIDE_BLENDER and Image is not None:
|
| 936 |
+
try:
|
| 937 |
+
visibility_info = compute_visibility_fractions(blender_objects)
|
| 938 |
+
except Exception as e:
|
| 939 |
+
print(f"Warning: compute_visibility_fractions failed during scene generation: {e}")
|
| 940 |
+
visibility_info = None
|
| 941 |
+
|
| 942 |
+
all_visible = True
|
| 943 |
+
if visibility_info is not None:
|
| 944 |
+
ratios, scene_counts, full_counts = visibility_info
|
| 945 |
+
min_ratio = min((r for r in ratios if full_counts[ratios.index(r)] > 0), default=1.0)
|
| 946 |
+
all_visible = all(
|
| 947 |
+
(full_counts[i] == 0) or (ratios[i] >= BASE_MIN_VISIBILITY_FRACTION)
|
| 948 |
+
for i in range(len(ratios))
|
| 949 |
+
)
|
| 950 |
+
if not all_visible:
|
| 951 |
+
print(f'Some objects are too occluded in generated scene; '
|
| 952 |
+
f'min visibility fraction={min_ratio:.3f} (required >= {BASE_MIN_VISIBILITY_FRACTION})')
|
| 953 |
+
else:
|
| 954 |
+
# Fallback to legacy absolute pixel-based visibility when PIL or Blender context is unavailable.
|
| 955 |
+
min_pixels = max(args.min_pixels_per_object, min_visible_pixels(args.width, args.height))
|
| 956 |
+
all_visible = check_visibility(blender_objects, min_pixels)
|
| 957 |
+
|
| 958 |
+
if not all_visible:
|
| 959 |
+
print('Some objects are occluded; replacing objects')
|
| 960 |
+
for obj in blender_objects:
|
| 961 |
+
delete_object(obj)
|
| 962 |
+
continue
|
| 963 |
+
|
| 964 |
+
return objects, blender_objects
|
| 965 |
+
|
| 966 |
+
raise RuntimeError(f"Failed to generate a valid scene after {max_scene_attempts} attempts")
|
| 967 |
+
|
| 968 |
+
|
| 969 |
+
def compute_all_relationships(scene_struct, eps=0.2):
|
| 970 |
+
"""
|
| 971 |
+
Computes relationships between all pairs of objects in the scene.
|
| 972 |
+
|
| 973 |
+
Returns a dictionary mapping string relationship names to lists of lists of
|
| 974 |
+
integers, where output[rel][i] gives a list of object indices that have the
|
| 975 |
+
relationship rel with object i. For example if j is in output['left'][i] then
|
| 976 |
+
object j is left of object j.
|
| 977 |
+
"""
|
| 978 |
+
all_relationships = {}
|
| 979 |
+
for name, direction_vec in scene_struct['directions'].items():
|
| 980 |
+
if name == 'above' or name == 'below': continue
|
| 981 |
+
all_relationships[name] = []
|
| 982 |
+
for i, obj1 in enumerate(scene_struct['objects']):
|
| 983 |
+
coords1 = obj1['3d_coords']
|
| 984 |
+
related = set()
|
| 985 |
+
for j, obj2 in enumerate(scene_struct['objects']):
|
| 986 |
+
if obj1 == obj2: continue
|
| 987 |
+
coords2 = obj2['3d_coords']
|
| 988 |
+
diff = [coords2[k] - coords1[k] for k in [0, 1, 2]]
|
| 989 |
+
dot = sum(diff[k] * direction_vec[k] for k in [0, 1, 2])
|
| 990 |
+
if dot > eps:
|
| 991 |
+
related.add(j)
|
| 992 |
+
all_relationships[name].append(sorted(list(related)))
|
| 993 |
+
return all_relationships
|
| 994 |
+
|
| 995 |
+
|
| 996 |
+
def compute_visibility_fractions(blender_objects):
|
| 997 |
+
if not INSIDE_BLENDER or not blender_objects:
|
| 998 |
+
return None
|
| 999 |
+
if Image is None:
|
| 1000 |
+
return None
|
| 1001 |
+
|
| 1002 |
+
# First pass: all objects together (occluded counts).
|
| 1003 |
+
fd, path = tempfile.mkstemp(suffix='.png')
|
| 1004 |
+
os.close(fd)
|
| 1005 |
+
try:
|
| 1006 |
+
colors_list = render_shadeless(blender_objects, path, use_distinct_colors=True)
|
| 1007 |
+
img = Image.open(path).convert('RGB')
|
| 1008 |
+
w, h = img.size
|
| 1009 |
+
pix = img.load()
|
| 1010 |
+
color_to_idx = {}
|
| 1011 |
+
for i, (r, g, b) in enumerate(colors_list):
|
| 1012 |
+
key = (round(r * 255), round(g * 255), round(b * 255))
|
| 1013 |
+
color_to_idx[key] = i
|
| 1014 |
+
scene_counts = [0] * len(blender_objects)
|
| 1015 |
+
for y in range(h):
|
| 1016 |
+
for x in range(w):
|
| 1017 |
+
key = (pix[x, y][0], pix[x, y][1], pix[x, y][2])
|
| 1018 |
+
if key in color_to_idx:
|
| 1019 |
+
scene_counts[color_to_idx[key]] += 1
|
| 1020 |
+
finally:
|
| 1021 |
+
try:
|
| 1022 |
+
os.remove(path)
|
| 1023 |
+
except Exception:
|
| 1024 |
+
pass
|
| 1025 |
+
|
| 1026 |
+
# Second pass: per-object "full area" with other objects hidden.
|
| 1027 |
+
full_counts = []
|
| 1028 |
+
original_hide_render = [obj.hide_render for obj in blender_objects]
|
| 1029 |
+
try:
|
| 1030 |
+
for idx, obj in enumerate(blender_objects):
|
| 1031 |
+
# Hide all other objects, ensure this one is visible.
|
| 1032 |
+
for j, other in enumerate(blender_objects):
|
| 1033 |
+
if j == idx:
|
| 1034 |
+
other.hide_render = False
|
| 1035 |
+
else:
|
| 1036 |
+
other.hide_render = True
|
| 1037 |
+
|
| 1038 |
+
fd_i, path_i = tempfile.mkstemp(suffix='.png')
|
| 1039 |
+
os.close(fd_i)
|
| 1040 |
+
try:
|
| 1041 |
+
colors_list = render_shadeless([obj], path_i, use_distinct_colors=True)
|
| 1042 |
+
img = Image.open(path_i).convert('RGB')
|
| 1043 |
+
w, h = img.size
|
| 1044 |
+
pix = img.load()
|
| 1045 |
+
color_to_idx = {}
|
| 1046 |
+
for i, (r, g, b) in enumerate(colors_list):
|
| 1047 |
+
key = (round(r * 255), round(g * 255), round(b * 255))
|
| 1048 |
+
color_to_idx[key] = i
|
| 1049 |
+
count = 0
|
| 1050 |
+
for y in range(h):
|
| 1051 |
+
for x in range(w):
|
| 1052 |
+
key = (pix[x, y][0], pix[x, y][1], pix[x, y][2])
|
| 1053 |
+
if key in color_to_idx:
|
| 1054 |
+
count += 1
|
| 1055 |
+
full_counts.append(count)
|
| 1056 |
+
finally:
|
| 1057 |
+
try:
|
| 1058 |
+
os.remove(path_i)
|
| 1059 |
+
except Exception:
|
| 1060 |
+
pass
|
| 1061 |
+
finally:
|
| 1062 |
+
# Restore previous hide_render flags.
|
| 1063 |
+
for obj, prev in zip(blender_objects, original_hide_render):
|
| 1064 |
+
obj.hide_render = prev
|
| 1065 |
+
|
| 1066 |
+
visibility = []
|
| 1067 |
+
for scene_c, full_c in zip(scene_counts, full_counts):
|
| 1068 |
+
if full_c <= 0:
|
| 1069 |
+
visibility.append(0.0)
|
| 1070 |
+
else:
|
| 1071 |
+
visibility.append(float(scene_c) / float(full_c))
|
| 1072 |
+
|
| 1073 |
+
return visibility, scene_counts, full_counts
|
| 1074 |
+
|
| 1075 |
+
|
| 1076 |
+
def check_visibility(blender_objects, min_pixels_per_object):
|
| 1077 |
+
"""
|
| 1078 |
+
Legacy absolute pixel-count visibility check, kept as a fallback when
|
| 1079 |
+
relative per-object visibility cannot be computed.
|
| 1080 |
+
"""
|
| 1081 |
+
if not INSIDE_BLENDER or not blender_objects:
|
| 1082 |
+
return True
|
| 1083 |
+
if Image is None:
|
| 1084 |
+
return True
|
| 1085 |
+
fd, path = tempfile.mkstemp(suffix='.png')
|
| 1086 |
+
os.close(fd)
|
| 1087 |
+
try:
|
| 1088 |
+
colors_list = render_shadeless(blender_objects, path, use_distinct_colors=True)
|
| 1089 |
+
img = Image.open(path).convert('RGB')
|
| 1090 |
+
w, h = img.size
|
| 1091 |
+
pix = img.load()
|
| 1092 |
+
color_to_idx = {}
|
| 1093 |
+
for i, (r, g, b) in enumerate(colors_list):
|
| 1094 |
+
key = (round(r * 255), round(g * 255), round(b * 255))
|
| 1095 |
+
color_to_idx[key] = i
|
| 1096 |
+
counts = [0] * len(blender_objects)
|
| 1097 |
+
for y in range(h):
|
| 1098 |
+
for x in range(w):
|
| 1099 |
+
key = (pix[x, y][0], pix[x, y][1], pix[x, y][2])
|
| 1100 |
+
if key in color_to_idx:
|
| 1101 |
+
counts[color_to_idx[key]] += 1
|
| 1102 |
+
all_visible = all(c >= min_pixels_per_object for c in counts)
|
| 1103 |
+
return all_visible
|
| 1104 |
+
finally:
|
| 1105 |
+
try:
|
| 1106 |
+
os.remove(path)
|
| 1107 |
+
except Exception:
|
| 1108 |
+
pass
|
| 1109 |
+
|
| 1110 |
+
|
| 1111 |
+
def render_shadeless(blender_objects, path='flat.png', use_distinct_colors=False):
|
| 1112 |
+
"""
|
| 1113 |
+
Render a version of the scene with shading disabled and unique materials
|
| 1114 |
+
assigned to all objects. The image itself is written to path. This is used to ensure
|
| 1115 |
+
that all objects will be visible in the final rendered scene (when check_visibility is enabled).
|
| 1116 |
+
Returns a list of (r,g,b) colors in object order (for visibility counting when use_distinct_colors=True).
|
| 1117 |
+
"""
|
| 1118 |
+
render_args = bpy.context.scene.render
|
| 1119 |
+
|
| 1120 |
+
old_filepath = render_args.filepath
|
| 1121 |
+
old_engine = render_args.engine
|
| 1122 |
+
|
| 1123 |
+
render_args.filepath = path
|
| 1124 |
+
render_args.engine = 'BLENDER_EEVEE_NEXT'
|
| 1125 |
+
|
| 1126 |
+
view_layer = bpy.context.scene.view_layers[0]
|
| 1127 |
+
old_use_pass_combined = view_layer.use_pass_combined
|
| 1128 |
+
|
| 1129 |
+
for obj_name in ['Lamp_Key', 'Lamp_Fill', 'Lamp_Back', 'Ground']:
|
| 1130 |
+
if obj_name in bpy.data.objects:
|
| 1131 |
+
obj = bpy.data.objects[obj_name]
|
| 1132 |
+
obj.hide_render = True
|
| 1133 |
+
|
| 1134 |
+
n = len(blender_objects)
|
| 1135 |
+
object_colors = [] if use_distinct_colors else set()
|
| 1136 |
+
old_materials = []
|
| 1137 |
+
for i, obj in enumerate(blender_objects):
|
| 1138 |
+
if len(obj.data.materials) > 0:
|
| 1139 |
+
old_materials.append(obj.data.materials[0])
|
| 1140 |
+
else:
|
| 1141 |
+
old_materials.append(None)
|
| 1142 |
+
|
| 1143 |
+
mat = bpy.data.materials.new(name='Material_%d' % i)
|
| 1144 |
+
mat.use_nodes = True
|
| 1145 |
+
nodes = mat.node_tree.nodes
|
| 1146 |
+
nodes.clear()
|
| 1147 |
+
|
| 1148 |
+
node_emission = nodes.new(type='ShaderNodeEmission')
|
| 1149 |
+
node_output = nodes.new(type='ShaderNodeOutputMaterial')
|
| 1150 |
+
|
| 1151 |
+
if use_distinct_colors:
|
| 1152 |
+
r = (i + 1) / (n + 1)
|
| 1153 |
+
g, b = 0.5, 0.5
|
| 1154 |
+
object_colors.append((r, g, b))
|
| 1155 |
+
else:
|
| 1156 |
+
while True:
|
| 1157 |
+
r, g, b = [random.random() for _ in range(3)]
|
| 1158 |
+
if (r, g, b) not in object_colors:
|
| 1159 |
+
break
|
| 1160 |
+
object_colors.add((r, g, b))
|
| 1161 |
+
|
| 1162 |
+
node_emission.inputs['Color'].default_value = (r, g, b, 1.0)
|
| 1163 |
+
mat.node_tree.links.new(node_emission.outputs['Emission'], node_output.inputs['Surface'])
|
| 1164 |
+
|
| 1165 |
+
if len(obj.data.materials) > 0:
|
| 1166 |
+
obj.data.materials[0] = mat
|
| 1167 |
+
else:
|
| 1168 |
+
obj.data.materials.append(mat)
|
| 1169 |
+
|
| 1170 |
+
bpy.ops.render.render(write_still=True)
|
| 1171 |
+
|
| 1172 |
+
for mat, obj in zip(old_materials, blender_objects):
|
| 1173 |
+
if mat is not None:
|
| 1174 |
+
obj.data.materials[0] = mat
|
| 1175 |
+
elif len(obj.data.materials) > 0:
|
| 1176 |
+
obj.data.materials.clear()
|
| 1177 |
+
|
| 1178 |
+
for obj_name in ['Lamp_Key', 'Lamp_Fill', 'Lamp_Back', 'Ground']:
|
| 1179 |
+
if obj_name in bpy.data.objects:
|
| 1180 |
+
obj = bpy.data.objects[obj_name]
|
| 1181 |
+
obj.hide_render = False
|
| 1182 |
+
|
| 1183 |
+
render_args.filepath = old_filepath
|
| 1184 |
+
render_args.engine = old_engine
|
| 1185 |
+
|
| 1186 |
+
return object_colors
|
| 1187 |
+
|
| 1188 |
+
|
| 1189 |
+
if __name__ == '__main__':
|
| 1190 |
+
if INSIDE_BLENDER:
|
| 1191 |
+
argv = extract_args()
|
| 1192 |
+
args = parser.parse_args(argv)
|
| 1193 |
+
main(args)
|
| 1194 |
+
elif '--help' in sys.argv or '-h' in sys.argv:
|
| 1195 |
+
parser.print_help()
|
| 1196 |
+
else:
|
| 1197 |
+
print('This script is intended to be called from blender like this:')
|
| 1198 |
+
print()
|
| 1199 |
+
print('blender --background --python render_images.py -- [args]')
|
| 1200 |
+
print()
|
| 1201 |
+
print('You can also run as a standalone python script to view all')
|
| 1202 |
+
print('arguments like this:')
|
| 1203 |
+
print()
|
| 1204 |
+
print('python render_images.py --help')
|