basic functionality - terrible performance
Grok didn't do a terribly greate job on this one. I'll clean it up with actually good code later.
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/stereograph.iml" filepath="$PROJECT_DIR$/.idea/stereograph.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
9
.idea/stereograph.iml
generated
Normal file
9
.idea/stereograph.iml
generated
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
7
go.mod
Normal file
7
go.mod
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module stereograph
|
||||||
|
|
||||||
|
go 1.23
|
||||||
|
|
||||||
|
require github.com/aws/aws-sdk-go v1.55.7
|
||||||
|
|
||||||
|
require github.com/jmespath/go-jmespath v0.4.0 // indirect
|
14
go.sum
Normal file
14
go.sum
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
|
||||||
|
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||||
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||||
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
155
main.go
Normal file
155
main.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
|
"github.com/aws/aws-sdk-go/service/s3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
useS3 bool
|
||||||
|
s3Bucket string
|
||||||
|
s3Region string
|
||||||
|
localDir string
|
||||||
|
port string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.BoolVar(&useS3, "s3", false, "Use S3")
|
||||||
|
flag.StringVar(&s3Bucket, "bucket", "", "S3 bucket name")
|
||||||
|
flag.StringVar(&s3Region, "region", "", "S3 region")
|
||||||
|
flag.StringVar(&localDir, "dir", "", "Local directory")
|
||||||
|
flag.StringVar(&port, "port", "8080", "Server port")
|
||||||
|
flag.Parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if useS3 && (s3Bucket == "" || s3Region == "") {
|
||||||
|
log.Fatal("S3 bucket and region must be provided when using S3")
|
||||||
|
}
|
||||||
|
if !useS3 && localDir == "" {
|
||||||
|
log.Fatal("Local directory must be provided when not using S3")
|
||||||
|
}
|
||||||
|
|
||||||
|
http.HandleFunc("/api/pairs", pairsHandler)
|
||||||
|
if !useS3 {
|
||||||
|
http.Handle("/images/", http.StripPrefix("/images/", http.FileServer(http.Dir(localDir))))
|
||||||
|
}
|
||||||
|
http.Handle("/", http.FileServer(http.Dir("web"))) // Serve static files from 'web' directory
|
||||||
|
log.Println("Server starting on port", port)
|
||||||
|
log.Fatal(http.ListenAndServe(":"+port, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func pairsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var pairs []Pair
|
||||||
|
if useS3 {
|
||||||
|
pairs = listS3Pairs()
|
||||||
|
} else {
|
||||||
|
pairs = listLocalPairs(r.Host)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(pairs)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pair struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Left string `json:"left"`
|
||||||
|
Right string `json:"right"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func listS3Pairs() []Pair {
|
||||||
|
sess, err := session.NewSession(&aws.Config{
|
||||||
|
Region: aws.String(s3Region),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error creating AWS session:", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s3Client := s3.New(sess)
|
||||||
|
resp, err := s3Client.ListObjectsV2(&s3.ListObjectsV2Input{
|
||||||
|
Bucket: aws.String(s3Bucket),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error listing S3 objects:", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pairMap := make(map[string][2]string)
|
||||||
|
for _, obj := range resp.Contents {
|
||||||
|
key := *obj.Key
|
||||||
|
if strings.HasSuffix(key, "_left.jpg") {
|
||||||
|
prefix := strings.TrimSuffix(key, "_left.jpg")
|
||||||
|
pairMap[prefix] = [2]string{key, ""}
|
||||||
|
} else if strings.HasSuffix(key, "_right.jpg") {
|
||||||
|
prefix := strings.TrimSuffix(key, "_right.jpg")
|
||||||
|
if val, ok := pairMap[prefix]; ok {
|
||||||
|
pairMap[prefix] = [2]string{val[0], key}
|
||||||
|
} else {
|
||||||
|
pairMap[prefix] = [2]string{"", key}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pairs []Pair
|
||||||
|
for prefix, keys := range pairMap {
|
||||||
|
if keys[0] != "" && keys[1] != "" {
|
||||||
|
leftReq, _ := s3Client.GetObjectRequest(&s3.GetObjectInput{
|
||||||
|
Bucket: aws.String(s3Bucket),
|
||||||
|
Key: aws.String(keys[0]),
|
||||||
|
})
|
||||||
|
leftURL, _ := leftReq.Presign(1 * time.Hour)
|
||||||
|
rightReq, _ := s3Client.GetObjectRequest(&s3.GetObjectInput{
|
||||||
|
Bucket: aws.String(s3Bucket),
|
||||||
|
Key: aws.String(keys[1]),
|
||||||
|
})
|
||||||
|
rightURL, _ := rightReq.Presign(1 * time.Hour)
|
||||||
|
pairs = append(pairs, Pair{Name: prefix, Left: leftURL, Right: rightURL})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pairs
|
||||||
|
}
|
||||||
|
|
||||||
|
func listLocalPairs(host string) []Pair {
|
||||||
|
files, err := os.ReadDir(localDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error reading local directory:", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pairMap := make(map[string][2]string)
|
||||||
|
for _, file := range files {
|
||||||
|
name := file.Name()
|
||||||
|
if strings.HasSuffix(name, "_left.jpg") {
|
||||||
|
prefix := strings.TrimSuffix(name, "_left.jpg")
|
||||||
|
pairMap[prefix] = [2]string{name, ""}
|
||||||
|
} else if strings.HasSuffix(name, "_right.jpg") {
|
||||||
|
prefix := strings.TrimSuffix(name, "_right.jpg")
|
||||||
|
if val, ok := pairMap[prefix]; ok {
|
||||||
|
pairMap[prefix] = [2]string{val[0], name}
|
||||||
|
} else {
|
||||||
|
pairMap[prefix] = [2]string{"", name}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := "http://" + host + "/images/"
|
||||||
|
var pairs []Pair
|
||||||
|
for prefix, names := range pairMap {
|
||||||
|
if names[0] != "" && names[1] != "" {
|
||||||
|
pairs = append(pairs, Pair{
|
||||||
|
Name: prefix,
|
||||||
|
Left: baseURL + names[0],
|
||||||
|
Right: baseURL + names[1],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pairs
|
||||||
|
}
|
175
web/index.html
Normal file
175
web/index.html
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Stereo Image Viewer</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; text-align: center; padding: 20px; }
|
||||||
|
#viewer { display: flex; justify-content: center; align-items: center; margin-top: 20px; }
|
||||||
|
#viewer img, #viewer canvas { max-width: 100%; height: auto; }
|
||||||
|
#methodControls { margin: 10px 0; }
|
||||||
|
select, button, input { margin: 5px; padding: 5px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Stereo Image Viewer</h1>
|
||||||
|
<label for="pairSelect">Select Image Pair:</label>
|
||||||
|
<select id="pairSelect"></select><br>
|
||||||
|
<label for="methodSelect">Viewing Method:</label>
|
||||||
|
<select id="methodSelect">
|
||||||
|
<option value="interlaced">Horizontally Interlaced</option>
|
||||||
|
<option value="switching">Actively Switching</option>
|
||||||
|
<option value="anaglyph">Red/Cyan Anaglyph</option>
|
||||||
|
<option value="sidebyside">Side by Side</option>
|
||||||
|
</select><br>
|
||||||
|
<button id="swapButton">Swap Left/Right</button>
|
||||||
|
<div id="methodControls"></div>
|
||||||
|
<div id="viewer"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentLeftUrl, currentRightUrl, pairs = [], intervalId;
|
||||||
|
|
||||||
|
function loadImage(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => resolve(img);
|
||||||
|
img.onerror = reject;
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayInterlaced(leftImg, rightImg) {
|
||||||
|
const startWithLeft = document.getElementById('swapInterlaced')?.checked ?? true;
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = leftImg.width;
|
||||||
|
canvas.height = leftImg.height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
for (let y = 0; y < canvas.height; y++) {
|
||||||
|
const sourceImg = (y % 2 === 0) ? (startWithLeft ? leftImg : rightImg) : (startWithLeft ? rightImg : leftImg);
|
||||||
|
ctx.drawImage(sourceImg, 0, y, canvas.width, 1, 0, y, canvas.width, 1);
|
||||||
|
}
|
||||||
|
document.getElementById('viewer').innerHTML = '';
|
||||||
|
document.getElementById('viewer').appendChild(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
function displaySwitching(leftUrl, rightUrl) {
|
||||||
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
const viewer = document.getElementById('viewer');
|
||||||
|
viewer.innerHTML = `<img id="switchImg" src="${leftUrl}">`;
|
||||||
|
const switchImg = document.getElementById('switchImg');
|
||||||
|
let showLeft = true;
|
||||||
|
intervalId = setInterval(() => {
|
||||||
|
switchImg.src = showLeft ? leftUrl : rightUrl;
|
||||||
|
showLeft = !showLeft;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayAnaglyph(leftImg, rightImg) {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = leftImg.width;
|
||||||
|
canvas.height = leftImg.height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.drawImage(leftImg, 0, 0);
|
||||||
|
const leftData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.drawImage(rightImg, 0, 0);
|
||||||
|
const rightData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
const anaglyphData = ctx.createImageData(canvas.width, canvas.height);
|
||||||
|
for (let i = 0; i < anaglyphData.data.length; i += 4) {
|
||||||
|
anaglyphData.data[i] = leftData.data[i]; // Red from left
|
||||||
|
anaglyphData.data[i+1] = rightData.data[i+1]; // Green from right
|
||||||
|
anaglyphData.data[i+2] = rightData.data[i+2]; // Blue from right
|
||||||
|
anaglyphData.data[i+3] = 255; // Alpha
|
||||||
|
}
|
||||||
|
ctx.putImageData(anaglyphData, 0, 0);
|
||||||
|
document.getElementById('viewer').innerHTML = '';
|
||||||
|
document.getElementById('viewer').appendChild(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
function displaySideBySide(leftUrl, rightUrl) {
|
||||||
|
const isCrossEye = document.getElementById('crossEye')?.checked ?? true;
|
||||||
|
const width = document.getElementById('widthSlider')?.value ?? 50;
|
||||||
|
const viewer = document.getElementById('viewer');
|
||||||
|
viewer.innerHTML = '';
|
||||||
|
const leftImg = document.createElement('img');
|
||||||
|
leftImg.src = leftUrl;
|
||||||
|
leftImg.style.width = width + '%';
|
||||||
|
const rightImg = document.createElement('img');
|
||||||
|
rightImg.src = rightUrl;
|
||||||
|
rightImg.style.width = width + '%';
|
||||||
|
if (isCrossEye) {
|
||||||
|
viewer.appendChild(rightImg);
|
||||||
|
viewer.appendChild(leftImg);
|
||||||
|
} else {
|
||||||
|
viewer.appendChild(leftImg);
|
||||||
|
viewer.appendChild(rightImg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateControls(method) {
|
||||||
|
const controls = document.getElementById('methodControls');
|
||||||
|
controls.innerHTML = '';
|
||||||
|
if (method === 'interlaced') {
|
||||||
|
controls.innerHTML = `
|
||||||
|
<label><input type="checkbox" id="swapInterlaced"> Start with Left</label>
|
||||||
|
`;
|
||||||
|
document.getElementById('swapInterlaced').addEventListener('change', () => loadAndDisplay(currentRightUrl, currentLeftUrl));
|
||||||
|
} else if (method === 'sidebyside') {
|
||||||
|
controls.innerHTML = `
|
||||||
|
<label><input type="checkbox" id="crossEye" checked> Cross-Eye (unchecked for Parallel)</label><br>
|
||||||
|
<label>Width: <input type="range" id="widthSlider" min="10" max="100" value="50"></label>
|
||||||
|
`;
|
||||||
|
document.getElementById('crossEye').addEventListener('change', () => loadAndDisplay(currentRightUrl, currentLeftUrl));
|
||||||
|
document.getElementById('widthSlider').addEventListener('input', () => loadAndDisplay(currentLeftUrl, currentRightUrl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAndDisplay(leftUrl, rightUrl) {
|
||||||
|
currentLeftUrl = leftUrl;
|
||||||
|
currentRightUrl = rightUrl;
|
||||||
|
const method = document.getElementById('methodSelect').value;
|
||||||
|
updateControls(method);
|
||||||
|
if (method === 'interlaced' || method === 'anaglyph') {
|
||||||
|
Promise.all([loadImage(leftUrl), loadImage(rightUrl)]).then(([leftImg, rightImg]) => {
|
||||||
|
if (method === 'interlaced') displayInterlaced(leftImg, rightImg);
|
||||||
|
else displayAnaglyph(leftImg, rightImg);
|
||||||
|
}).catch(err => console.error('Image load failed:', err));
|
||||||
|
} else if (method === 'switching') {
|
||||||
|
displaySwitching(leftUrl, rightUrl);
|
||||||
|
} else if (method === 'sidebyside') {
|
||||||
|
displaySideBySide(leftUrl, rightUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('methodSelect').addEventListener('change', () => {
|
||||||
|
if (currentLeftUrl && currentRightUrl) loadAndDisplay(currentLeftUrl, currentRightUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('swapButton').addEventListener('click', () => {
|
||||||
|
if (currentLeftUrl && currentRightUrl) {
|
||||||
|
[currentLeftUrl, currentRightUrl] = [currentRightUrl, currentLeftUrl];
|
||||||
|
loadAndDisplay(currentLeftUrl, currentRightUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch('/api/pairs')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
pairs = data;
|
||||||
|
const select = document.getElementById('pairSelect');
|
||||||
|
data.forEach(pair => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = pair.name;
|
||||||
|
option.text = pair.name;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
select.onchange = () => {
|
||||||
|
const selectedPair = pairs.find(p => p.name === select.value);
|
||||||
|
if (selectedPair) loadAndDisplay(selectedPair.left, selectedPair.right);
|
||||||
|
};
|
||||||
|
if (data.length > 0) loadAndDisplay(data[0].left, data[0].right);
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Failed to fetch pairs:', err));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Reference in New Issue
Block a user