Slide 1

Slide 1 text

綺麗に撮れすぎるスマホカメラからブレを取り戻す|genshi 綺麗に撮れすぎる スマホカメラからブレを取り戻す genshi

Slide 2

Slide 2 text

綺麗に撮れすぎるスマホカメラからブレを取り戻す|genshi product|pixiv genshi • ピクシブ株式会社 26卒 iOSアプリエンジニア • pixivの iOSアプリの開発を担当 • アイマス, 麻雀, ノベルゲーム 2 2

Slide 3

Slide 3 text

綺麗に撮れすぎるスマホカメラからブレを取り戻す|genshi はじめに

Slide 4

Slide 4 text

「写真」撮ってる? 4

Slide 5

Slide 5 text

僕は撮ってます 5

Slide 6

Slide 6 text

ブレブレの写真を 6

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

ブレブレの写真を どう撮るか 10

Slide 11

Slide 11 text

カメラを高速で近づけたり遠ざけたりする 11

Slide 12

Slide 12 text

像全体が拡大・縮小 12

Slide 13

Slide 13 text

各点が中心から外側に移動し広がる 13

Slide 14

Slide 14 text

シャッターに各点の移動が軌跡として映る 14

Slide 15

Slide 15 text

ごはんとエンカウント 15

Slide 16

Slide 16 text

ブレブレの写真を 撮るのって難しい 16

Slide 17

Slide 17 text

いくつか問題が 17

Slide 18

Slide 18 text

問題1 18

Slide 19

Slide 19 text

最近のiPhoneの 性能が良すぎる 19

Slide 20

Slide 20 text

20 https://www.apple.com/jp/iphone-17-pro/

Slide 21

Slide 21 text

21 https://www.apple.com/jp/iphone-18-pro/

Slide 22

Slide 22 text

性能が上がると困ること • シャッターが速い 22

Slide 23

Slide 23 text

性能が上がると困ること • シャッターが速い • 軌跡が写るには露光中に像が動く必要がある 23

Slide 24

Slide 24 text

性能が上がると困ること • シャッターが速い • 軌跡が写るには露光中に像が動く必要がある • シャッターが速くなると像の移動量も減る 24

Slide 25

Slide 25 text

性能が上がると困ること • シャッターが速い • 軌跡が写るには露光中に像が動く必要がある • シャッターが速くなると像の移動量も減る • 手ブレ補正 25

Slide 26

Slide 26 text

性能が上がると困ること • シャッターが速い • 軌跡が写るには露光中に像が動く必要がある • シャッターが速くなると像の移動量も減る • 手ブレ補正 • 揺れがレンズやセンサー側の逆方向の動きで相殺される 26

Slide 27

Slide 27 text

性能が上がると困ること • シャッターが速い • 軌跡が写るには露光中に像が動く必要がある • シャッターが速くなると像の移動量も減る • 手ブレ補正 • 揺れがレンズやセンサー側の逆方向の動きで相殺される • Computational Photography (Smart HDR, Deep Fusionなど) 27

Slide 28

Slide 28 text

趣味でブレた写真 撮ってる人が困る😭 28

Slide 29

Slide 29 text

問題2 29

Slide 30

Slide 30 text

カメラを高速で近づけたり遠ざけたりするのが恥ずかしい 30

Slide 31

Slide 31 text

カメラを高速で近づけたり遠ざけたりするのが恥ずかしい 31

Slide 32

Slide 32 text

同期からの声 32

Slide 33

Slide 33 text

コイツとご飯行く とスマホ振り回して て恥ずかしいな… 33

Slide 34

Slide 34 text

iOSエンジニアなら 技術で解決 💪 34

Slide 35

Slide 35 text

綺麗に撮れすぎるスマホカメラからブレを取り戻す|genshi 本題

Slide 36

Slide 36 text

アプローチ1 36

Slide 37

Slide 37 text

高速でレンズを 動かす 37

Slide 38

Slide 38 text

setFocusModeLoc ked(lensPosition:) 38 https://developer.apple.com/documentation/avfoundation/avcapturedevice/ setfocusmodelocked(lensposition:completionhandler:)

Slide 39

Slide 39 text

レンズの位置を制御 39

Slide 40

Slide 40 text

カメラデバイスの存在チェック nonisolated func stepHardwareSweep() { guard let captureDevice else { return } sweepPhase += Self.sweepUpdateInterval / Self.sweepPeriod if sweepPhase >= 1 { sweepPhase -= 1 } // Smooth sine wave 0 -> 1 -> 0 shared by both drives. let wave = 0.5 - 0.5 * cos(2 * Double.pi * sweepPhase) do { try captureDevice.lockForConfiguration() if captureDevice.isLockingFocusWithCustomLensPositionSupported { captureDevice.setFocusModeLocked(lensPosition: Float(wave)) } let maxZoom = min(Self.sweepMaxZoomFactor, captureDevice.activeFormat.videoMaxZoomFactor) captureDevice.videoZoomFactor = 1 + (maxZoom - 1) * CGFloat(wave) } captureDevice.unlockForConfiguration() } catch { return } 40

Slide 41

Slide 41 text

レンズ位置とズーム倍率の目標値を作成 nonisolated func stepHardwareSweep() { guard let captureDevice else { return } sweepPhase += Self.sweepUpdateInterval / Self.sweepPeriod if sweepPhase >= 1 { sweepPhase -= 1 } // Smooth sine wave 0 -> 1 -> 0 shared by both drives. let wave = 0.5 - 0.5 * cos(2 * Double.pi * sweepPhase) do { try captureDevice.lockForConfiguration() if captureDevice.isLockingFocusWithCustomLensPositionSupported { captureDevice.setFocusModeLocked(lensPosition: Float(wave)) } let maxZoom = min(Self.sweepMaxZoomFactor, captureDevice.activeFormat.videoMaxZoomFactor) captureDevice.videoZoomFactor = 1 + (maxZoom - 1) * CGFloat(wave) } captureDevice.unlockForConfiguration() } catch { return } 41

Slide 42

Slide 42 text

カメラに反映 nonisolated func stepHardwareSweep() { guard let captureDevice else { return } sweepPhase += Self.sweepUpdateInterval / Self.sweepPeriod if sweepPhase >= 1 { sweepPhase -= 1 } // Smooth sine wave 0 -> 1 -> 0 shared by both drives. let wave = 0.5 - 0.5 * cos(2 * Double.pi * sweepPhase) do { try captureDevice.lockForConfiguration() if captureDevice.isLockingFocusWithCustomLensPositionSupported { captureDevice.setFocusModeLocked(lensPosition: Float(wave)) } let maxZoom = min(Self.sweepMaxZoomFactor, captureDevice.activeFormat.videoMaxZoomFactor) captureDevice.videoZoomFactor = 1 + (maxZoom - 1) * CGFloat(wave) } captureDevice.unlockForConfiguration() } catch { return } 42

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

なんか違う… 44

Slide 45

Slide 45 text

ぼやけて滲んでいるだけで軌跡が無い 45

Slide 46

Slide 46 text

もっと集中線や ブラーが欲しい! 46

Slide 47

Slide 47 text

アプローチ2 47

Slide 48

Slide 48 text

画像処理 48

Slide 49

Slide 49 text

📱→ 49

Slide 50

Slide 50 text

📱→ 画像処理😭 50

Slide 51

Slide 51 text

📱→ 画像処理😭 逆にブレ → 51

Slide 52

Slide 52 text

📱→ 画像処理😭 逆にブレ → 👍 52

Slide 53

Slide 53 text

フレームを20枚くらい撮ります guard var burst = activeBurst else { return } let now = CACurrentMediaTime() guard now >= burst.startTime else { return } if now >= burst.nextFrameTime, let frame = makeFrame(from: sampleBuffer) { burst.frames.append(CapturedFrame(image: frame, timestamp: now, motion: latestMotion)) burst.nextFrameTime = now + burst.frameInterval activeBurst = burst } 53

Slide 54

Slide 54 text

少しづつ拡大&透明にして重ねます for layer in 1...layerCount { let progress = CGFloat(layer) / CGFloat(layerCount) let scale = config.minScale + (config.maxScale - config.minScale) * progress let direction: CGFloat = layer.isMultiple(of: 2) ? 1 : -1 let rotation = config.rotationJitter * direction * progress let transform = CGAffineTransform(translationX: center.x, y: center.y) .rotated(by: rotation) .scaledBy(x: scale, y: scale) .translatedBy(x: -center.x, y: -center.y) let scaled = source.transformed(by: transform).cropped(to: extent) let alphaShaped: CIImage if let mask = maskImage { alphaShaped = scaled.applyingFilter( "CIBlendWithMask", parameters: [ "inputBackgroundImage": transparent, "inputMaskImage": mask ] ) } else { alphaShaped = scaled } } let weighted = alphaShaped.applyingFilter( "CIColorMatrix", parameters: [ "inputAVector": CIVector(x: 0, y: 0, z: 0, w: config.layerAlpha) ] ) output = weighted.composited(over: output).cropped(to: extent)

Slide 55

Slide 55 text

センサの値を使って 移動量を決める 55

Slide 56

Slide 56 text

56

Slide 57

Slide 57 text

いい感じに ブレた気がする 57

Slide 58

Slide 58 text

ズームブラーで盛る 58

Slide 59

Slide 59 text

CIFilter 59 https://developer.apple.com/documentation/coreimage/cifilter-swift.class

Slide 60

Slide 60 text

中心は透明・外周は不透明のようなマスクを作る let maskImage = CIFilter( name: "CIRadialGradient", parameters: [ "inputCenter": CIVector(x: center.x, y: center.y), "inputRadius0": innerRadius, "inputRadius1": outerRadius, "inputColor0": CIColor(red: 0, green: 0, blue: 0, alpha: 0), "inputColor1": CIColor(red: 1, green: 1, blue: 1, alpha: 1) ] )?.outputImage?.cropped(to: extent) 60

Slide 61

Slide 61 text

中心は透明・外周は不透明のようなマスクを作る let maskImage = CIFilter( name: "CIRadialGradient", parameters: [ "inputCenter": CIVector(x: center.x, y: center.y), "inputRadius0": innerRadius, "inputRadius1": outerRadius, "inputColor0": CIColor(red: 0, green: 0, blue: 0, alpha: 0), "inputColor1": CIColor(red: 1, green: 1, blue: 1, alpha: 1) ] )?.outputImage?.cropped(to: extent) 61

Slide 62

Slide 62 text

中心は透明・外周は不透明のようなマスクを作る let maskImage = CIFilter( name: "CIRadialGradient", parameters: [ "inputCenter": CIVector(x: center.x, y: center.y), "inputRadius0": innerRadius, "inputRadius1": outerRadius, "inputColor0": CIColor(red: 0, green: 0, blue: 0, alpha: 0), "inputColor1": CIColor(red: 1, green: 1, blue: 1, alpha: 1) ] )?.outputImage?.cropped(to: extent) 62

Slide 63

Slide 63 text

中心は透明・外周は不透明のようなマスクを作る let maskImage = CIFilter( name: "CIRadialGradient", parameters: [ "inputCenter": CIVector(x: center.x, y: center.y), "inputRadius0": innerRadius, "inputRadius1": outerRadius, "inputColor0": CIColor(red: 0, green: 0, blue: 0, alpha: 0), "inputColor1": CIColor(red: 1, green: 1, blue: 1, alpha: 1) ] )?.outputImage?.cropped(to: extent) 63

Slide 64

Slide 64 text

No content

Slide 65

Slide 65 text

綺麗に撮れすぎるスマホカメラからブレを取り戻す|genshi DEMO

Slide 66

Slide 66 text

66

Slide 67

Slide 67 text

綺麗に撮れすぎるスマホカメラからブレを取り戻す|genshi おわりに

Slide 68

Slide 68 text

みなさんも 躍動感ある写真 撮ってみませんか? 68

Slide 69

Slide 69 text

みなさんも 躍動感ある写真 撮ってみませんか? 69

Slide 70

Slide 70 text

綺麗に撮れすぎるスマホカメラからブレを取り戻す|genshi 綺麗に撮れすぎる スマホカメラからブレを取り戻す genshi