Initial commit
This commit is contained in:
commit
7b60283303
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
unslop-cv
|
688
cmd/unslop-cv.go
Normal file
688
cmd/unslop-cv.go
Normal file
@ -0,0 +1,688 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rwcarlsen/goexif/exif"
|
||||||
|
"gocv.io/x/gocv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CameraProfile struct {
|
||||||
|
Make string
|
||||||
|
Model string
|
||||||
|
FocalLength float64
|
||||||
|
CameraMatrix gocv.Mat
|
||||||
|
DistCoeffs gocv.Mat
|
||||||
|
}
|
||||||
|
|
||||||
|
type PhotoCorrector struct {
|
||||||
|
cameraProfiles map[string]CameraProfile
|
||||||
|
defaultProfile CameraProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPhotoCorrector() *PhotoCorrector {
|
||||||
|
pc := &PhotoCorrector{
|
||||||
|
cameraProfiles: make(map[string]CameraProfile),
|
||||||
|
}
|
||||||
|
pc.loadCameraProfiles()
|
||||||
|
return pc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) loadCameraProfiles() {
|
||||||
|
pc.defaultProfile = pc.createDefaultProfile()
|
||||||
|
|
||||||
|
pc.cameraProfiles["Canon_EOS R5"] = pc.createCanonEOSR5Profile()
|
||||||
|
pc.cameraProfiles["Nikon_D850"] = pc.createNikonD850Profile()
|
||||||
|
pc.cameraProfiles["Sony_A7R IV"] = pc.createSonyA7R4Profile()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) createDefaultProfile() CameraProfile {
|
||||||
|
fx := 1800.0
|
||||||
|
fy := 1800.0
|
||||||
|
cx := 2000.0
|
||||||
|
cy := 1500.0
|
||||||
|
|
||||||
|
cameraMatrix := gocv.NewMatWithSize(3, 3, gocv.MatTypeCV64F)
|
||||||
|
cameraMatrix.SetDoubleAt(0, 0, fx)
|
||||||
|
cameraMatrix.SetDoubleAt(0, 1, 0)
|
||||||
|
cameraMatrix.SetDoubleAt(0, 2, cx)
|
||||||
|
cameraMatrix.SetDoubleAt(1, 0, 0)
|
||||||
|
cameraMatrix.SetDoubleAt(1, 1, fy)
|
||||||
|
cameraMatrix.SetDoubleAt(1, 2, cy)
|
||||||
|
cameraMatrix.SetDoubleAt(2, 0, 0)
|
||||||
|
cameraMatrix.SetDoubleAt(2, 1, 0)
|
||||||
|
cameraMatrix.SetDoubleAt(2, 2, 1)
|
||||||
|
|
||||||
|
distCoeffs := gocv.NewMatWithSize(1, 5, gocv.MatTypeCV64F)
|
||||||
|
distCoeffs.SetDoubleAt(0, 0, -0.3)
|
||||||
|
distCoeffs.SetDoubleAt(0, 1, 0.15)
|
||||||
|
distCoeffs.SetDoubleAt(0, 2, 0.0)
|
||||||
|
distCoeffs.SetDoubleAt(0, 3, 0.0)
|
||||||
|
distCoeffs.SetDoubleAt(0, 4, -0.05)
|
||||||
|
|
||||||
|
return CameraProfile{
|
||||||
|
Make: "Generic",
|
||||||
|
Model: "WideAngle",
|
||||||
|
FocalLength: 16.0,
|
||||||
|
CameraMatrix: cameraMatrix,
|
||||||
|
DistCoeffs: distCoeffs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) createCanonEOSR5Profile() CameraProfile {
|
||||||
|
fx := 2100.0
|
||||||
|
fy := 2100.0
|
||||||
|
cx := 2250.0
|
||||||
|
cy := 1832.0
|
||||||
|
|
||||||
|
cameraMatrix := gocv.NewMatWithSize(3, 3, gocv.MatTypeCV64F)
|
||||||
|
cameraMatrix.SetDoubleAt(0, 0, fx)
|
||||||
|
cameraMatrix.SetDoubleAt(0, 1, 0)
|
||||||
|
cameraMatrix.SetDoubleAt(0, 2, cx)
|
||||||
|
cameraMatrix.SetDoubleAt(1, 0, 0)
|
||||||
|
cameraMatrix.SetDoubleAt(1, 1, fy)
|
||||||
|
cameraMatrix.SetDoubleAt(1, 2, cy)
|
||||||
|
cameraMatrix.SetDoubleAt(2, 0, 0)
|
||||||
|
cameraMatrix.SetDoubleAt(2, 1, 0)
|
||||||
|
cameraMatrix.SetDoubleAt(2, 2, 1)
|
||||||
|
|
||||||
|
distCoeffs := gocv.NewMatWithSize(1, 5, gocv.MatTypeCV64F)
|
||||||
|
distCoeffs.SetDoubleAt(0, 0, -0.25)
|
||||||
|
distCoeffs.SetDoubleAt(0, 1, 0.12)
|
||||||
|
distCoeffs.SetDoubleAt(0, 2, 0.001)
|
||||||
|
distCoeffs.SetDoubleAt(0, 3, 0.001)
|
||||||
|
distCoeffs.SetDoubleAt(0, 4, -0.02)
|
||||||
|
|
||||||
|
return CameraProfile{
|
||||||
|
Make: "Canon",
|
||||||
|
Model: "EOS R5",
|
||||||
|
FocalLength: 15.0,
|
||||||
|
CameraMatrix: cameraMatrix,
|
||||||
|
DistCoeffs: distCoeffs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) createNikonD850Profile() CameraProfile {
|
||||||
|
fx := 1950.0
|
||||||
|
fy := 1950.0
|
||||||
|
cx := 2259.0
|
||||||
|
cy := 1752.0
|
||||||
|
|
||||||
|
cameraMatrix := gocv.NewMatWithSize(3, 3, gocv.MatTypeCV64F)
|
||||||
|
cameraMatrix.SetDoubleAt(0, 0, fx)
|
||||||
|
cameraMatrix.SetDoubleAt(0, 1, 0)
|
||||||
|
cameraMatrix.SetDoubleAt(0, 2, cx)
|
||||||
|
cameraMatrix.SetDoubleAt(1, 0, 0)
|
||||||
|
cameraMatrix.SetDoubleAt(1, 1, fy)
|
||||||
|
cameraMatrix.SetDoubleAt(1, 2, cy)
|
||||||
|
cameraMatrix.SetDoubleAt(2, 0, 0)
|
||||||
|
cameraMatrix.SetDoubleAt(2, 1, 0)
|
||||||
|
cameraMatrix.SetDoubleAt(2, 2, 1)
|
||||||
|
|
||||||
|
distCoeffs := gocv.NewMatWithSize(1, 5, gocv.MatTypeCV64F)
|
||||||
|
distCoeffs.SetDoubleAt(0, 0, -0.28)
|
||||||
|
distCoeffs.SetDoubleAt(0, 1, 0.14)
|
||||||
|
distCoeffs.SetDoubleAt(0, 2, 0.002)
|
||||||
|
distCoeffs.SetDoubleAt(0, 3, 0.001)
|
||||||
|
distCoeffs.SetDoubleAt(0, 4, -0.03)
|
||||||
|
|
||||||
|
return CameraProfile{
|
||||||
|
Make: "Nikon",
|
||||||
|
Model: "D850",
|
||||||
|
FocalLength: 14.0,
|
||||||
|
CameraMatrix: cameraMatrix,
|
||||||
|
DistCoeffs: distCoeffs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) createSonyA7R4Profile() CameraProfile {
|
||||||
|
fx := 2000.0
|
||||||
|
fy := 2000.0
|
||||||
|
cx := 4784.0
|
||||||
|
cy := 3168.0
|
||||||
|
|
||||||
|
cameraMatrix := gocv.NewMatWithSize(3, 3, gocv.MatTypeCV64F)
|
||||||
|
cameraMatrix.SetDoubleAt(0, 0, fx)
|
||||||
|
cameraMatrix.SetDoubleAt(0, 1, 0)
|
||||||
|
cameraMatrix.SetDoubleAt(0, 2, cx)
|
||||||
|
cameraMatrix.SetDoubleAt(1, 0, 0)
|
||||||
|
cameraMatrix.SetDoubleAt(1, 1, fy)
|
||||||
|
cameraMatrix.SetDoubleAt(1, 2, cy)
|
||||||
|
cameraMatrix.SetDoubleAt(2, 0, 0)
|
||||||
|
cameraMatrix.SetDoubleAt(2, 1, 0)
|
||||||
|
cameraMatrix.SetDoubleAt(2, 2, 1)
|
||||||
|
|
||||||
|
distCoeffs := gocv.NewMatWithSize(1, 5, gocv.MatTypeCV64F)
|
||||||
|
distCoeffs.SetDoubleAt(0, 0, -0.22)
|
||||||
|
distCoeffs.SetDoubleAt(0, 1, 0.10)
|
||||||
|
distCoeffs.SetDoubleAt(0, 2, 0.0015)
|
||||||
|
distCoeffs.SetDoubleAt(0, 3, 0.0012)
|
||||||
|
distCoeffs.SetDoubleAt(0, 4, -0.015)
|
||||||
|
|
||||||
|
return CameraProfile{
|
||||||
|
Make: "Sony",
|
||||||
|
Model: "ILCE-7RM4",
|
||||||
|
FocalLength: 16.0,
|
||||||
|
CameraMatrix: cameraMatrix,
|
||||||
|
DistCoeffs: distCoeffs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type EXIFData struct {
|
||||||
|
Make string
|
||||||
|
Model string
|
||||||
|
FocalLength float64
|
||||||
|
Aperture float64
|
||||||
|
ISO int
|
||||||
|
ImageWidth int
|
||||||
|
ImageHeight int
|
||||||
|
LensModel string
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractEXIFData(filename string) (*EXIFData, error) {
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
x, err := exif.Decode(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := &EXIFData{}
|
||||||
|
|
||||||
|
if make, err := x.Get(exif.Make); err == nil {
|
||||||
|
data.Make = strings.TrimSpace(make.String())
|
||||||
|
}
|
||||||
|
if model, err := x.Get(exif.Model); err == nil {
|
||||||
|
data.Model = strings.TrimSpace(model.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if focal, err := x.Get(exif.FocalLength); err == nil {
|
||||||
|
if focalVal, err := focal.Rat(0); err == nil {
|
||||||
|
f, _ := focalVal.Float64()
|
||||||
|
data.FocalLength = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if aperture, err := x.Get(exif.FNumber); err == nil {
|
||||||
|
if apertureVal, err := aperture.Rat(0); err == nil {
|
||||||
|
f, _ := apertureVal.Float64()
|
||||||
|
data.Aperture = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if iso, err := x.Get(exif.ISOSpeedRatings); err == nil {
|
||||||
|
if isoVal, err := iso.Int(0); err == nil {
|
||||||
|
data.ISO = isoVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if width, err := x.Get(exif.ImageWidth); err == nil {
|
||||||
|
if widthVal, err := width.Int(0); err == nil {
|
||||||
|
data.ImageWidth = widthVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if height, err := x.Get(exif.ImageLength); err == nil {
|
||||||
|
if heightVal, err := height.Int(0); err == nil {
|
||||||
|
data.ImageHeight = heightVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) getCameraMatrix(exifData *EXIFData, width, height int) gocv.Mat {
|
||||||
|
sensorWidth := 36.0
|
||||||
|
if exifData.FocalLength > 0 {
|
||||||
|
fx := (exifData.FocalLength / sensorWidth) * float64(width)
|
||||||
|
fy := fx
|
||||||
|
cx := float64(width) / 2.0
|
||||||
|
cy := float64(height) / 2.0
|
||||||
|
|
||||||
|
cameraMatrix := gocv.NewMatWithSize(3, 3, gocv.MatTypeCV64F)
|
||||||
|
cameraMatrix.SetDoubleAt(0, 0, fx)
|
||||||
|
cameraMatrix.SetDoubleAt(0, 2, cx)
|
||||||
|
cameraMatrix.SetDoubleAt(1, 1, fy)
|
||||||
|
cameraMatrix.SetDoubleAt(1, 2, cy)
|
||||||
|
cameraMatrix.SetDoubleAt(2, 2, 1.0)
|
||||||
|
|
||||||
|
return cameraMatrix
|
||||||
|
}
|
||||||
|
|
||||||
|
return pc.defaultProfile.CameraMatrix.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) getDistortionCoefficients(exifData *EXIFData) gocv.Mat {
|
||||||
|
profileKey := fmt.Sprintf("%s_%s", exifData.Make, exifData.Model)
|
||||||
|
|
||||||
|
if profile, exists := pc.cameraProfiles[profileKey]; exists {
|
||||||
|
return profile.DistCoeffs.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
if exifData.FocalLength > 0 && exifData.FocalLength < 20 {
|
||||||
|
|
||||||
|
k1 := -0.2 + (20.0-exifData.FocalLength)*0.02
|
||||||
|
k2 := 0.1 + (20.0-exifData.FocalLength)*0.01
|
||||||
|
|
||||||
|
distCoeffs := gocv.NewMatWithSize(1, 5, gocv.MatTypeCV64F)
|
||||||
|
distCoeffs.SetDoubleAt(0, 0, k1)
|
||||||
|
distCoeffs.SetDoubleAt(0, 1, k2)
|
||||||
|
distCoeffs.SetDoubleAt(0, 2, 0.0)
|
||||||
|
distCoeffs.SetDoubleAt(0, 3, 0.0)
|
||||||
|
distCoeffs.SetDoubleAt(0, 4, 0.0)
|
||||||
|
|
||||||
|
return distCoeffs
|
||||||
|
}
|
||||||
|
|
||||||
|
return pc.defaultProfile.DistCoeffs.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) correctLensDistortion(img gocv.Mat, exifData *EXIFData) gocv.Mat {
|
||||||
|
height, width := img.Rows(), img.Cols()
|
||||||
|
|
||||||
|
cameraMatrix := pc.getCameraMatrix(exifData, width, height)
|
||||||
|
distCoeffs := pc.getDistortionCoefficients(exifData)
|
||||||
|
|
||||||
|
defer cameraMatrix.Close()
|
||||||
|
defer distCoeffs.Close()
|
||||||
|
|
||||||
|
corrected := gocv.NewMat()
|
||||||
|
|
||||||
|
gocv.Undistort(img, &corrected, cameraMatrix, distCoeffs, cameraMatrix)
|
||||||
|
|
||||||
|
return corrected
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) detectVanishingPoints(img gocv.Mat) []gocv.Point2f {
|
||||||
|
gray := gocv.NewMat()
|
||||||
|
defer gray.Close()
|
||||||
|
gocv.CvtColor(img, &gray, gocv.ColorBGRToGray)
|
||||||
|
|
||||||
|
edges := gocv.NewMat()
|
||||||
|
defer edges.Close()
|
||||||
|
gocv.Canny(gray, &edges, 50, 150)
|
||||||
|
|
||||||
|
lines := gocv.NewMat()
|
||||||
|
defer lines.Close()
|
||||||
|
gocv.HoughLines(edges, &lines, 1, math.Pi/180, 100)
|
||||||
|
|
||||||
|
vanishingPoints := make([]gocv.Point2f, 0)
|
||||||
|
|
||||||
|
if lines.Rows() > 0 {
|
||||||
|
|
||||||
|
vp1 := gocv.Point2f{X: float32(img.Cols() / 2), Y: float32(img.Rows() / 2)}
|
||||||
|
vanishingPoints = append(vanishingPoints, vp1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return vanishingPoints
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) calculatePerspectiveCorrectionStrength(focalLength float64) float64 {
|
||||||
|
if focalLength <= 0 {
|
||||||
|
return 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case focalLength < 16:
|
||||||
|
return 1.5
|
||||||
|
case focalLength < 24:
|
||||||
|
return 1.2
|
||||||
|
case focalLength < 35:
|
||||||
|
return 1.0
|
||||||
|
default:
|
||||||
|
return 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) correctPerspective(img gocv.Mat, exifData *EXIFData) gocv.Mat {
|
||||||
|
height, width := img.Rows(), img.Cols()
|
||||||
|
|
||||||
|
correctionStrength := pc.calculatePerspectiveCorrectionStrength(exifData.FocalLength)
|
||||||
|
|
||||||
|
if exifData.FocalLength > 85 {
|
||||||
|
return img.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
vanishingPoints := pc.detectVanishingPoints(img)
|
||||||
|
|
||||||
|
if len(vanishingPoints) == 0 {
|
||||||
|
return img.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
srcPoints := []image.Point{
|
||||||
|
{X: 0, Y: 0},
|
||||||
|
{X: width, Y: 0},
|
||||||
|
{X: width, Y: height},
|
||||||
|
{X: 0, Y: height},
|
||||||
|
}
|
||||||
|
|
||||||
|
margin := int(math.Round(float64(width) * 0.05 * correctionStrength))
|
||||||
|
dstPoints := []image.Point{
|
||||||
|
{X: margin, Y: margin},
|
||||||
|
{X: width - margin, Y: margin},
|
||||||
|
{X: width - margin, Y: height - margin},
|
||||||
|
{X: margin, Y: height - margin},
|
||||||
|
}
|
||||||
|
|
||||||
|
transformMatrix := gocv.GetPerspectiveTransform(gocv.NewPointVectorFromPoints(srcPoints), gocv.NewPointVectorFromPoints(dstPoints))
|
||||||
|
defer transformMatrix.Close()
|
||||||
|
|
||||||
|
size := img.Size()
|
||||||
|
imageSize := image.Pt(size[1], size[0])
|
||||||
|
|
||||||
|
corrected := gocv.NewMat()
|
||||||
|
gocv.WarpPerspective(img, &corrected, transformMatrix, imageSize)
|
||||||
|
|
||||||
|
return corrected
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) enhanceForRealEstate(img gocv.Mat, exifData *EXIFData) gocv.Mat {
|
||||||
|
enhanced := img.Clone()
|
||||||
|
|
||||||
|
if exifData.Aperture > 0 && exifData.ISO > 0 {
|
||||||
|
|
||||||
|
alpha := float32(1.0)
|
||||||
|
beta := float32(0.0)
|
||||||
|
|
||||||
|
if exifData.ISO > 800 {
|
||||||
|
alpha = 1.1
|
||||||
|
}
|
||||||
|
if exifData.Aperture < 2.8 {
|
||||||
|
beta = 10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
tempMat := gocv.NewMat()
|
||||||
|
enhanced.ConvertTo(&tempMat, gocv.MatTypeCV8U)
|
||||||
|
tempMat.MultiplyFloat(alpha)
|
||||||
|
tempMat.AddFloat(beta)
|
||||||
|
enhanced = tempMat
|
||||||
|
}
|
||||||
|
|
||||||
|
hsv := gocv.NewMat()
|
||||||
|
defer hsv.Close()
|
||||||
|
gocv.CvtColor(enhanced, &hsv, gocv.ColorBGRToHSV)
|
||||||
|
|
||||||
|
channels := gocv.Split(hsv)
|
||||||
|
defer func() {
|
||||||
|
for _, ch := range channels {
|
||||||
|
ch.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
tempSat := gocv.NewMat()
|
||||||
|
channels[1].ConvertTo(&tempSat, gocv.MatTypeCV8U)
|
||||||
|
tempSat.MultiplyFloat(1.1)
|
||||||
|
channels[1] = tempSat
|
||||||
|
|
||||||
|
gocv.Merge(channels, &hsv)
|
||||||
|
gocv.CvtColor(hsv, &enhanced, gocv.ColorHSVToBGR)
|
||||||
|
|
||||||
|
return enhanced
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) ProcessImage(inputPath, outputPath string) error {
|
||||||
|
img := gocv.IMRead(inputPath, gocv.IMReadColor)
|
||||||
|
if img.Empty() {
|
||||||
|
return fmt.Errorf("failed to load image: %s", inputPath)
|
||||||
|
}
|
||||||
|
defer img.Close()
|
||||||
|
|
||||||
|
exifData, err := extractEXIFData(inputPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: Could not extract EXIF data: %v", err)
|
||||||
|
|
||||||
|
exifData = &EXIFData{
|
||||||
|
Make: "Unknown",
|
||||||
|
Model: "Unknown",
|
||||||
|
FocalLength: 16.0,
|
||||||
|
ImageWidth: img.Cols(),
|
||||||
|
ImageHeight: img.Rows(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Processing image with EXIF data: %+v\n", exifData)
|
||||||
|
|
||||||
|
fmt.Println("Correcting lens distortion...")
|
||||||
|
distortionCorrected := pc.correctLensDistortion(img, exifData)
|
||||||
|
defer distortionCorrected.Close()
|
||||||
|
|
||||||
|
fmt.Println("Correcting perspective...")
|
||||||
|
perspectiveCorrected := pc.correctPerspective(distortionCorrected, exifData)
|
||||||
|
defer perspectiveCorrected.Close()
|
||||||
|
|
||||||
|
fmt.Println("Applying real estate enhancements...")
|
||||||
|
enhanced := pc.enhanceForRealEstate(perspectiveCorrected, exifData)
|
||||||
|
defer enhanced.Close()
|
||||||
|
|
||||||
|
ok := gocv.IMWrite(outputPath, enhanced)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("failed to save corrected image: %s", outputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Successfully processed image: %s -> %s\n", inputPath, outputPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) CalibrateCamera(calibrationImages []string, boardSize image.Point) (*CameraProfile, error) {
|
||||||
|
if len(calibrationImages) == 0 {
|
||||||
|
return nil, fmt.Errorf("no calibration images provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
objectPoints := make([][]gocv.Point3f, 0)
|
||||||
|
objectPointsVector := gocv.NewPoints3fVectorFromPoints(objectPoints)
|
||||||
|
imagePoints := make([][]gocv.Point2f, 0)
|
||||||
|
imagePointsVector := gocv.NewPoints2fVectorFromPoints(imagePoints)
|
||||||
|
|
||||||
|
objp := make([]gocv.Point3f, 0)
|
||||||
|
for i := range boardSize.Y {
|
||||||
|
for j := range boardSize.X {
|
||||||
|
objp = append(objp, gocv.Point3f{
|
||||||
|
X: float32(j), Y: float32(i), Z: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageSize image.Point
|
||||||
|
|
||||||
|
fmt.Printf("Processing %d calibration images...\n", len(calibrationImages))
|
||||||
|
for i, imagePath := range calibrationImages {
|
||||||
|
img := gocv.IMRead(imagePath, gocv.IMReadColor)
|
||||||
|
if img.Empty() {
|
||||||
|
fmt.Printf("Warning: Could not load image %s\n", imagePath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if imageSize.X == 0 {
|
||||||
|
size := img.Size()
|
||||||
|
imageSize = image.Pt(size[1], size[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
gray := gocv.NewMat()
|
||||||
|
gocv.CvtColor(img, &gray, gocv.ColorBGRToGray)
|
||||||
|
|
||||||
|
cornerPoint2f := make([]gocv.Point2f, 0)
|
||||||
|
corners := gocv.NewMatFromPoint2fVector(gocv.NewPoint2fVectorFromPoints(cornerPoint2f), true)
|
||||||
|
|
||||||
|
found := gocv.FindChessboardCorners(gray, boardSize, &corners,
|
||||||
|
gocv.CalibCBAdaptiveThresh|gocv.CalibCBFastCheck|gocv.CalibCBNormalizeImage)
|
||||||
|
|
||||||
|
if found {
|
||||||
|
|
||||||
|
criteria := gocv.NewTermCriteria(gocv.Count|gocv.EPS, 30, 0.01)
|
||||||
|
|
||||||
|
gocv.CornerSubPix(gray, &corners, image.Pt(11, 11),
|
||||||
|
image.Pt(-1, -1), criteria)
|
||||||
|
|
||||||
|
objectPoints = append(objectPoints, objp)
|
||||||
|
imagePoints = append(imagePoints, cornerPoint2f)
|
||||||
|
fmt.Printf("Found corners in image %d/%d\n", i+1, len(calibrationImages))
|
||||||
|
} else {
|
||||||
|
fmt.Printf("No corners found in image %d/%d\n", i+1, len(calibrationImages))
|
||||||
|
}
|
||||||
|
|
||||||
|
img.Close()
|
||||||
|
gray.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(objectPoints) < 5 {
|
||||||
|
return nil, fmt.Errorf("need at least 5 successful calibration images, got %d", len(objectPoints))
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraMatrix := gocv.NewMat()
|
||||||
|
distCoeffs := gocv.NewMat()
|
||||||
|
rvecs := gocv.NewMat()
|
||||||
|
tvecs := gocv.NewMat()
|
||||||
|
|
||||||
|
fmt.Printf("Calibrating camera with %d image pairs...\n", len(objectPoints))
|
||||||
|
rms := gocv.CalibrateCamera(objectPointsVector, imagePointsVector, imageSize,
|
||||||
|
&cameraMatrix, &distCoeffs, &rvecs, &tvecs, 0)
|
||||||
|
|
||||||
|
fmt.Printf("Camera calibration completed with RMS error: %f\n", rms)
|
||||||
|
|
||||||
|
profile := &CameraProfile{
|
||||||
|
Make: "Calibrated",
|
||||||
|
Model: "Custom",
|
||||||
|
FocalLength: 0,
|
||||||
|
CameraMatrix: cameraMatrix,
|
||||||
|
DistCoeffs: distCoeffs,
|
||||||
|
}
|
||||||
|
|
||||||
|
rvecs.Close()
|
||||||
|
tvecs.Close()
|
||||||
|
|
||||||
|
return profile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) BatchProcess(inputDir, outputDir string) error {
|
||||||
|
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create output directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(inputDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read input directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
processedCount := 0
|
||||||
|
errorCount := 0
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := strings.ToLower(filepath.Ext(entry.Name()))
|
||||||
|
if ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".tiff" && ext != ".bmp" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
inputPath := filepath.Join(inputDir, entry.Name())
|
||||||
|
outputPath := filepath.Join(outputDir, entry.Name())
|
||||||
|
|
||||||
|
fmt.Printf("Processing: %s\n", entry.Name())
|
||||||
|
if err := pc.ProcessImage(inputPath, outputPath); err != nil {
|
||||||
|
log.Printf("Error processing %s: %v", entry.Name(), err)
|
||||||
|
errorCount++
|
||||||
|
} else {
|
||||||
|
processedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Batch processing completed: %d successful, %d errors\n", processedCount, errorCount)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) AddCameraProfile(key string, profile CameraProfile) {
|
||||||
|
pc.cameraProfiles[key] = profile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) GetSupportedCameras() []string {
|
||||||
|
keys := make([]string, 0, len(pc.cameraProfiles))
|
||||||
|
for k := range pc.cameraProfiles {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) SaveCameraProfile(profile *CameraProfile, filename string) error {
|
||||||
|
return fmt.Errorf("not implemented - would save profile to %s", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PhotoCorrector) LoadCameraProfile(filename string) (*CameraProfile, error) {
|
||||||
|
return nil, fmt.Errorf("not implemented - would load profile from %s", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Println("Real Estate Photo Corrector")
|
||||||
|
fmt.Println("Usage:")
|
||||||
|
fmt.Println(" Single image: go run main.go <input_image> [output_image]")
|
||||||
|
fmt.Println(" Batch process: go run main.go -batch <input_dir> <output_dir>")
|
||||||
|
fmt.Println(" Show supported cameras: go run main.go -cameras")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pc := NewPhotoCorrector()
|
||||||
|
defer func() {
|
||||||
|
pc.defaultProfile.CameraMatrix.Close()
|
||||||
|
pc.defaultProfile.DistCoeffs.Close()
|
||||||
|
|
||||||
|
for _, profile := range pc.cameraProfiles {
|
||||||
|
profile.CameraMatrix.Close()
|
||||||
|
profile.DistCoeffs.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "-batch":
|
||||||
|
if len(os.Args) < 4 {
|
||||||
|
fmt.Println("Batch processing requires input and output directories")
|
||||||
|
fmt.Println("Usage: go run main.go -batch <input_dir> <output_dir>")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
inputDir := os.Args[2]
|
||||||
|
outputDir := os.Args[3]
|
||||||
|
|
||||||
|
fmt.Printf("Batch processing images from %s to %s\n", inputDir, outputDir)
|
||||||
|
if err := pc.BatchProcess(inputDir, outputDir); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "-cameras":
|
||||||
|
fmt.Println("Supported camera profiles:")
|
||||||
|
for _, camera := range pc.GetSupportedCameras() {
|
||||||
|
fmt.Printf(" - %s\n", camera)
|
||||||
|
}
|
||||||
|
fmt.Printf(" - %s (default)\n", "Generic_WideAngle")
|
||||||
|
|
||||||
|
default:
|
||||||
|
|
||||||
|
inputPath := os.Args[1]
|
||||||
|
outputPath := "corrected_" + filepath.Base(inputPath)
|
||||||
|
if len(os.Args) > 2 {
|
||||||
|
outputPath = os.Args[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
|
||||||
|
fmt.Printf("Error: Input file %s does not exist\n", inputPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Processing single image: %s -> %s\n", inputPath, outputPath)
|
||||||
|
fmt.Printf("Supported cameras: %v\n", pc.GetSupportedCameras())
|
||||||
|
|
||||||
|
if err := pc.ProcessImage(inputPath, outputPath); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println("Image processing completed!")
|
||||||
|
}
|
||||||
|
}
|
8
go.mod
Normal file
8
go.mod
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
module unslop-cv
|
||||||
|
|
||||||
|
go 1.24.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
|
||||||
|
gocv.io/x/gocv v0.41.0
|
||||||
|
)
|
4
go.sum
Normal file
4
go.sum
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
|
||||||
|
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||||
|
gocv.io/x/gocv v0.41.0 h1:KM+zRXUP28b6dHfhy+4JxDODbCNQNtLg8kio+YE7TqA=
|
||||||
|
gocv.io/x/gocv v0.41.0/go.mod h1:zYdWMj29WAEznM3Y8NsU3A0TRq/wR/cy75jeUypThqU=
|
Loading…
x
Reference in New Issue
Block a user