Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Avatar for genshi genshi
September 12, 2026
360

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

iOSDC Japan 2026 (https://fortee.jp/iosdc-japan-2026/proposal/32fb9648-3596-4302-a1bd-c00627701b4c)
ルーキーズLT(5分) 発表資料

Avatar for genshi

genshi

September 12, 2026

Transcript

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

    揺れがレンズやセンサー側の逆方向の動きで相殺される • Computational Photography (Smart HDR, Deep Fusionなど) 27
  2. カメラデバイスの存在チェック 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
  3. レンズ位置とズーム倍率の目標値を作成 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
  4. カメラに反映 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
  5. フレームを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
  6. 少しづつ拡大&透明にして重ねます 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)
  7. 56

  8. 中心は透明・外周は不透明のようなマスクを作る 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
  9. 中心は透明・外周は不透明のようなマスクを作る 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
  10. 中心は透明・外周は不透明のようなマスクを作る 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
  11. 中心は透明・外周は不透明のようなマスクを作る 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
  12. 66